Block and Logic Nets

Block and LogicNet are lower level PyRTL abstractions for representing a hardware design. Most users won’t need to understand them, unless they are implementing Analysis and Optimization passes or modifying PyRTL itself.

GateGraphs are an alternative representation that makes it easier to write analysis passes.

Blocks

class pyrtl.Block[source]

Block encapsulates a netlist.

A Block in PyRTL is the class that stores a netlist and provides basic access and error checking members. Each Block has well defined Inputs and Outputs, and contains both the basic logic elements and references to the WireVectors and MemBlocks that connect them together.

The logic structure is primarily contained in logic which holds a set of LogicNets. Each LogicNet describes a primitive operation (such as an adder or memory), a 4-tuple of:

  1. The op (a single character describing the operation such as + or r).

  2. A set of static op_param for the operation, such as the bit slices to select for the s “selection” operation.

  3. A tuple args containing the WireVectors connected as inputs to the LogicNet.

  4. A tuple dests containing the WireVectors connected as outputs from the LogicNet.

Below is a list of the basic operations. These properties (more formally specified) should all be checked by sanity_check().

  • Most logical and arithmetic ops are pretty self explanatory. Each takes exactly two args, and they should perform the arithmetic or logical operation specified.

    ops: &, |, ^, n, ~, +, -, *.

    All inputs must be the same bitwidth. Logical operations produce as many bits as are in the input, while + and - produce n + 1 bits, and * produces 2 * n bits.

  • In addition there are some operations for performing comparisons that should perform the operation specified. The = op is checking to see if the bits of the args vectors are equal, while < and > do unsigned arithmetic comparison. All comparisons generate a single bit dest (1 for True, 0 for False).

  • The w op is simply a directional wire that connects args to dests. It has no logic function.

  • The x op is a multiplexer which takes a select bit and two signals as args. If the value of the select bit is 0 it selects the second arg; if it is 1 it selects the third arg. Select must be a single bit, while the other two args must be the same length.

  • The c op is the concatenation operator and combines any number of WireVector args (a, b, …, z) into a single new WireVector with a in the MSB and z (or whatever is last) in the LSB position.

  • The s op is the selection operator and chooses, based on the op_param specified, a subset of the logic bits from a WireVector to select. Repeats are accepted.

  • The r op is a register and on posedge, simply copies the value from arg to the register’s dest.

  • The m op is a memory block read port, which supports async reads (acting like combinational logic). Multiple read (and write) ports are possible to the same memory but each m defines only one of those. The op_param is a tuple containing two values: the MemBlock.id (memid), and a reference to the MemBlock containing this port. The MemBlock should only be used for debug and sanity checks. Each read port has one addr (an arg) and one data (a dest).

  • The @ (update) op is a memory block write port, which supports synchronous writes (writes are “latched” at positive edge). Multiple write (and read) ports are possible to the same memory but each @ defines only one of those. The op_param is a tuple containing two values: the MemBlock.id (memid), and a reference to the MemBlock. Writes have three args (addr, data, and write enable we_en). The dests should be an empty tuple. You will not see a written value change until the following cycle. If multiple writes happen to the same address in the same cycle the behavior is currently undefined.

The connecting elements (args and dests) should be WireVectors or derived from WireVector, and should be registered with the Block using add_wirevector(). LogicNets should be registered using add_net().

In addition, there is a member legal_ops which defines the set of operations that can be legally added to the Block. By default it is set to all of the above defined operations, but it can be useful in certain cases to only allow a subset of operations (such as when transforms are being done that are “lowering” the Block to more primitive ops).

See LogicNet for a complete list of defined operations.

add_net(net)[source]

Add logic to the Block.

The passed LogicNet is checked and added to the Block. No WireVectors are added; they must be added seperately with add_wirevector().

Parameters:

net (LogicNet) – LogicNet to add to self.

add_wirevector(wirevector)[source]
Parameters:

wirevector (WireVector) – WireVector to add to self.

get_memblock_by_name(name, strict=False)[source]

Get a MemBlock from the Block, by name.

Useful for getting access to internal MemBlocks for testing. For example, the Simulation constructor requires a MemBlock reference for its memory_value_map. Instead of passing the MemBlock around in your code, you can get a reference to the MemBlock from the global working_block():

>>> def read_hidden_mem(read_addr: pyrtl.WireVector) -> pyrtl.WireVector:
...     mem = pyrtl.MemBlock(bitwidth=8, addrwidth=4, name='hidden_mem')
...     return mem[read_addr]

>>> read_addr = pyrtl.Input(4, 'read_addr')
>>> data = pyrtl.Output(8, 'data')
>>> data <<= read_hidden_mem(read_addr)

>>> hidden_mem = pyrtl.working_block().get_memblock_by_name('hidden_mem')
>>> sim = pyrtl.Simulation(memory_value_map={hidden_mem: {3: 7}})
>>> sim.step(provided_inputs={read_addr.name: 3})
>>> sim.inspect(data.name)
7

Returns None if no matching MemBlock can be found. However, if strict=True, then this will instead throw a PyrtlError when no match is found.

Parameters:
  • name (str) – Name of the MemBlock to retrieve.

  • strict (bool, default: False) – When True, raises an exception when no match is found. Defaults to False.

Raises:

PyrtlError – When strict=True and no match is found.

Return type:

MemBlock

Returns:

The MemBlock object with specified name

get_wirevector_by_name(name, strict=False)[source]

Return the WireVector with matching name.

Parameters:
  • name (str) – Name of WireVector.

  • strict (bool, default: False) – If True, raise an exception when no matching WireVector is found. Defaults to False.

Raises:

PyrtlError – if strict=True and no matching WireVector is found.

Return type:

WireVector

Returns:

The WireVector with the specified name.

legal_ops: set[str]

Set of allowed ops in this block.

logic: set[LogicNet]

Set of LogicNets belonging to this Block.

logic_subset(op=None)[source]

Return a subset of the Block's LogicNets.

Filters LogicNets by their op.

If op is None, returns all the LogicNets associated with the Block. Otherwise, returns a set of the LogicNets with one of the specified ops. This is helpful for getting all memories of a block for example.

Parameters:

op (tuple[str] | None, default: None) – LogicNet.op to filter on. Defaults to None.

Return type:

set[LogicNet]

Returns:

LogicNets in the Block with the corresponding op.

net_connections(include_virtual_nodes=False)[source]

Returns sources and sinks for each WireVector in the Block.

A WireVector’s source is the LogicNet that sets the WireVector’s value.

A WireVector’s destinations are the LogicNets that use the WireVector’s value.

This information helps when building a graph representation for the Block. See net_graph() for an example.

Note

Consider using GateGraphs instead.

Parameters:

include_virtual_nodes (bool, default: False) – If True, external sources (such as an Inputs and Consts) will be represented as wires that set themselves, and external destinations (such as Outputs) will be represented as wires that use themselves. If False, these nodes will be excluded from the results.

Return type:

tuple[dict[WireVector, LogicNet], dict[WireVector, list[LogicNet]]]

Returns:

Two dictionaries. The first maps WireVectors to the LogicNet that creates their signal (wire_src_dict). The second maps WireVectors to a list of LogicNets that use their signal (wire_dst_dict).

remove_wirevector(wirevector)[source]
Parameters:

wirevector (WireVector) – WireVector to remove from self.

sanity_check()[source]

Verify the Block’s integrity. Raise an exception if there is an issue.

sanity_check does not change any of the Block’s state. It only verifies that the Block’s data structures are internally consistent.

Raises:

PyrtlError – If the Block is malformed.

wirevector_subset(cls=None, exclude=())[source]

Return a subset of the Block's WireVectors.

Filters WireVectors by type.

If cls is None, returns all the WireVectors associated with the Block. If cls is a single type, or a tuple of types, only WireVectors of the matching types will be returned. This is helpful for getting all of a Block’s Inputs, Outputs, or Registers for example:

>>> len([pyrtl.Input(bitwidth=4) for _ in range(2)])
2
>>> len([pyrtl.Output(bitwidth=4) for _ in range(3)])
3
>>> block = pyrtl.working_block()

>>> inputs = block.wirevector_subset(pyrtl.Input)
>>> len(inputs)
2
>>> all(isinstance(input, pyrtl.Input) for input in inputs)
True

>>> non_inputs = block.wirevector_subset(exclude=pyrtl.Input)
>>> len(non_inputs)
3
>>> any(isinstance(non_input, pyrtl.Input) for non_input in non_inputs)
False
Parameters:
Return type:

set[WireVector]

Returns:

WireVectors in the Block that are both a cls type and not an excluded type.

working_block

Most PyRTL operations operate on the global working_block by default. PyRTL provides several functions to inspect and manipulate the working_block:

pyrtl.working_block(block=None)[source]

Convenience function for capturing the current working block.

If a block is not passed, or if the block passed is None, then this will return the “current working block”. However, if a block is passed in it will simply return block instead. This feature is useful in allowing functions to “override” the current working block.

Return type:

Block

pyrtl.reset_working_block()[source]

Reset the working block to be empty.

pyrtl.set_working_block(block, no_sanity_check=False)[source]

Set the working block to be the block passed as argument. Compatible with the with statement.

Sanity checks will only be run if the new block is different from the original block.

pyrtl.temp_working_block()[source]

Set the working block to be new temporary block.

If used with the with statement the block will be reset to the original value (at the time of call) at exit of the context.

LogicNets

class pyrtl.LogicNet(op: str, op_param: tuple, args: tuple[WireVector], dests: tuple[WireVector])[source]

The basic immutable datatype for storing a “net” in a netlist.

LogicNet is part of PyRTL’s internal representation. What it is, and how it is used are only required for advanced PyRTL users.

A “net” is a Python representation of a hardware logic operation. These operations include binary operations like and or and not, arithmetic operations like + and -, and other operations like concatenating wires, splitting wires, reading/writing memory, and register logic:

op

op_param

args

dests

&

None

a1, a2

out

AND two wires together, put result into out

|

None

a1, a2

out

OR two wires together, put result into out

^

None

a1, a2

out

XOR two wires together, put result into out

n

None

a1, a2

out

NAND two wires together, put result into out

~

None

a1

out

Invert one wire, put result into out.

+

None

a1, a2

out

Add a1 and a2, put result into out.

len(out) == max(len(a1), len(a2)) + 1

Performs unsigned addition. Use signed_add() for signed addition.

-

None

a1, a2

out

Subtract a2 from a1, put result into out.

len(out) == max(len(a1), len(a2)) + 1

Performs unsigned subtraction. Use signed_sub() for signed subtraction.

*

None

a1, a2

out

Multiply a1 and a2, put result into out.

len(out) == len(a1) + len(a2)

Performs unsigned multiplication. Use signed_mult() for signed multiplication.

=

None

a1, a2

out

Check if a1 and a2 are equal, put result into out. out has bitwidth 1.

<

None

a1, a2

out

Check if a1 is less than a2, put result into out. out has bitwidth 1.

Performs unsigned comparison. Use signed_lt() for signed less than.

>

None

a1, a2

out

Check if a1 is greater than a2, put result into out out has bitwidth 1.

Performs unsigned comparison. Use signed_gt() for signed greater than.

w

None

w1

w2

Connects w1 to w2. This is a directional wire with no logical function.

x

None

x, a1, a2

out

Multiplexer:

When x == 0, connect a1 to out.

When x == 1, connect a2 to out.

x must be 1-bit and len(a1) == len(a2).

c

None

*args

out

Concatenates args into a single WireVector.

The first arg becomes the most significant bits, and the last arg becomes the least significant bits.

s

sel

wire

out

Selects bits from wire based on sel (slicing syntax).

Puts the selected bits into out.

r

None

next

r1

On positive clock edge: copy next to r1.

m

memid, mem

addr

data

Read address addr of MemBlock mem (with id memid), put the data read into data.

@

memid, mem

addr data, wr_en

Write data to MemBlock mem (with id memid) at address addr, if wr_en is 1. This is the only op with no dests.

args: tuple[WireVector]

Input arguments to the operation.

dests: tuple[WireVector]

Output of the operation.

op: str

Operation performed by the LogicNet.

op_param: tuple

Static parameters for the operation.

GateGraphs

GateGraph is an alternative representation for PyRTL logic.

Motivation

PyRTL represents logic internally with WireVectors and LogicNets. For example, the following code creates five WireVectors and two LogicNets:

>>> a = pyrtl.Input(name="a", bitwidth=1)
>>> b = pyrtl.Input(name="b", bitwidth=1)
>>> c = pyrtl.Input(name="c", bitwidth=1)

>>> x = a & b
>>> x.name = "x"

>>> y = x | c
>>> y.name = "y"

>>> print(pyrtl.working_block())
x/1W <-- & -- a/1I, b/1I
y/1W <-- | -- x/1W, c/1I

The WireVectors and LogicNets are arranged like this:

┌──────────────┐
│ LogicNet "&" │
│     op: "&"  │    ┌────────────────┐
│     args:────┼───▶│ WireVector "a" │
│     args:────┼─┐  └────────────────┘
│              │ │  ┌────────────────┐
│              │ └─▶│ WireVector "b" │
│              │    └────────────────┘
│              │    ┌────────────────┐
│     dests:───┼───▶│ WireVector "x" │
└──────────────┘ ┌─▶└────────────────┘
┌──────────────┐ │
│ LogicNet "|" │ │
│     op: "|"  │ │
│     args:────┼─┘  ┌────────────────┐
│     args:────┼───▶│ WireVector "c" │
│              │    └────────────────┘
│              │    ┌────────────────┐
│     dests:───┼───▶│ WireVector "y" │
└──────────────┘    └────────────────┘

This data structure is difficult to work with for three reasons:

  1. The arrows do not consistently point from producer to consumer, or from consumer to producer. For example, there is no arrow from WireVector x (producer) to LogicNet | (consumer). Similarly, there is no arrow from WireVector x (consumer) to LogicNet & (producer). These missing arrows make it impossible to iteratively traverse the data structure. This creates a need for methods like net_connections(), which creates wire_src_dict and wire_sink_dict with the missing pointers.

  2. The data structure is composed of two different classes, LogicNet and WireVector, and these two classes have completely different interfaces. As we follow pointers from one class to another, we must keep track of the current object’s class, and interact with it appropriately.

  3. WireVector is part of PyRTL’s user interface, but also a key part of PyRTL’s internal representation. This makes WireVector complex and difficult to modify, because it must implement user-facing features like inferring bitwidth from assignment, while also maintaining a consistent internal representation for simulation, analysis, and optimization.

GateGraph is an alternative representation that addresses these issues. A GateGraph is just a collection of Gates, so we’ll cover Gate first.

class pyrtl.Gate(logic_net=None, wire_vector=None, args=None)[source]

Gate is an alternative to PyRTL’s default LogicNet and WireVector representation.

Gate makes it easy to iteratively explore a circuit, while simplifying the circuit’s representation by making everything a Gate. A Gate is equivalent to a LogicNet fused with its dest WireVector. So this LogicNet and WireVector:

┌──────────────────┐
│ LogicNet         │    ┌───────────────────┐
│     op: o        │    │ WireVector        │
│     args: [x, y] │    │     name: n       │
│     dests:───────┼───▶│     bitwidth: b   │
└──────────────────┘    └───────────────────┘

Are equivalent to this Gate:

┌─────────────────────┐
│ Gate                │
│     op: o           │
│     args: [x, y]    │
│     name: n         │
│     bitwidth: b     │
│     dests: [g1, g2] │
└─────────────────────┘

Key differences between the two representations:

  1. The Gate’s args [x, y] are references to other Gates.

  2. The WireVector’s name and bitwidth are stored as Gate.name and Gate.bitwidth. If the LogicNet produces no output, like a MemBlock write, the Gate’s name and bitwidth will be None. PyRTL does not have an op with multiple LogicNet.dests.

  3. The Gate has a new Gate.dests attribute, which has no direct equivalent in the LogicNet/WireVector representation. Gate.dests is a list of the Gates that use this Gate’s output as one of their args.

LogicNet.dests and Gate.dests represent slightly different things, despite having similar names:

With Gates, the example from the Motivation section looks like:

┌─────────────────┐
│ Gate "a"        │
│     op: "I"     │
│     name: "a"   │
│     bitwidth: 1 │    ┌─────────────────┐
│     dests:──────┼───▶│ Gate "&"        │
└─────────────────┘◀─┐ │     op: "&"     │
┌─────────────────┐  └─┼─────args        │
│ Gate "b"        │◀───┼─────args        │
│     op: "I"     │    │     name: "x"   │    ┌─────────────────┐
│     name: "b"   │    │     bitwidth: 1 │    │ Gate "|"        │
│     bitwidth: 1 │ ┌─▶│     dests:──────┼───▶│     op: "|"     │
│     dests:──────┼─┘  └─────────────────┘◀───┼─────args        │
└─────────────────┘  ┌────────────────────────┼─────args        │
┌─────────────────┐  │                        │     name: "y"   │
│ Gate "c"        │◀─┘   ┌───────────────────▶│     bitwidth: 1 │
│     op: "I"     │      │                    └─────────────────┘
│     name: "c"   │      │
│     bitwidth: 1 │      │
│     dests:──────┼──────┘
└─────────────────┘

With a Gate representation, it is easy to iteratively traverse the data structure:

  1. Forwards, from producer to consumer, by following dests references.

  2. Backwards, from consumer to producer, by following args references.

The Gate representation addresses the issues raised in the Motivation section:

  1. The Gate representation is easy to iteratively explore by following args and dests references, which are shown as arrows in the figure above.

  2. There is only one class in the Gate graph, so we don’t need to keep track of the current object’s type as we follow arrows in the graph, like we did with LogicNet and WireVector. Everything is a Gate.

  3. By decoupling the Gate representation from WireVector and LogicNet, Gate specializes in supporting analysis use cases, without the burden of supporting all of WireVector’s other features. This significantly simplifies Gate’s design and implementation.

For usage examples, see GateGraph and Gate’s documentation below.

__str__()[source]
Return type:

str

Returns:

A string representation of this Gate.

Example:

>>> a = pyrtl.Input(name="a", bitwidth=8)
>>> bit_slice = a[2:4]
>>> bit_slice.name = "bit_slice"

>>> gate_graph = pyrtl.GateGraph()
>>> bit_slice_gate = gate_graph.get_gate("bit_slice")

>>> print(bit_slice_gate)
bit_slice/2 = slice(a/8) [sel=(2, 3)]

In this sample string representation:

  • name is bit_slice.

  • bitwidth is 2.

  • op is s, spelled out as slice to improve readability.

  • args is [<Gate for "a">]:

    >>> bit_slice_gate.args[0] is gate_graph.get_gate("a")
    True
    
  • op_param is (2, 3), written as sel because a slice’s op_param determines the selected bits. This improves readability by indicating what the op_param means for the op.

args: list[Gate]

Inputs to the operation. Corresponds to LogicNet.args.

For each Gate arg in self.args, self is in arg.dests.

Some special Gates represent operations without args, like Input and Const. Such operations will have an empty list of args.

Note

The same Gate may appear multiple times in args. A self-loop Register Gate may be its own arg.

Example:

>>> a = pyrtl.Input(name="a", bitwidth=1)
>>> b = pyrtl.Input(name="b", bitwidth=1)
>>> c = pyrtl.Input(name="c", bitwidth=1)
>>> abc = pyrtl.concat(a, b, c)
>>> abc.name = "abc"

>>> gate_graph = pyrtl.GateGraph()
>>> abc_gate = gate_graph.get_gate("abc")
>>> [gate.name for gate in abc_gate.args]
['a', 'b', 'c']
bitwidth: int | None

Bitwidth of the operation’s output. Corresponds to WireVector.bitwidth.

Some operations do not have outputs, like MemBlock writes. These operations will have a bitwidth of None.

Example:

>>> a = pyrtl.Input(name="a", bitwidth=1)
>>> b = pyrtl.Input(name="b", bitwidth=1)
>>> ab = a + b
>>> ab.name = "ab"

>>> gate_graph = pyrtl.GateGraph()
>>> ab_gate = gate_graph.get_gate("ab")
>>> ab_gate.bitwidth
2
property const_value: int

Readability alias for op_param, returning the value for a Const (C) op.

Example:

>>> const = pyrtl.Const(name="const", val=33)

>>> gate_graph = pyrtl.GateGraph()
>>> const_gate = gate_graph.get_gate("const")
>>> const_gate.const_value
33
dests: list[Gate]

list of Gates that use this operation’s output as one of their args.

For each Gate dest in self.dests, self is in dest.args.

Note

The same Gate may appear multiple times in dests. A self-loop Register Gate may appear in its own dests.

Example:

>>> a = pyrtl.Input(name="a", bitwidth=1)
>>> _ = a + 1
>>> _ = a - 1

>>> gate_graph = pyrtl.GateGraph()
>>> a_gate = gate_graph.get_gate("a")
>>> [gate.op for gate in a_gate.dests]
['+', '-']
is_output: bool

Indicates if the operation’s output is an Output.

Example:

>>> a = pyrtl.Input(name="a", bitwidth=1)
>>> b = pyrtl.Output(name="b", bitwidth=1)
>>> b <<= a

>>> gate_graph = pyrtl.GateGraph()
>>> a_gate = gate_graph.get_gate("a")
>>> a_gate.is_output
False
>>> b_gate = gate_graph.get_gate("b")
>>> b_gate.is_output
True
property mem: int

Readability alias for op_param, returning the MemBlock for a read (m) or write (@) op.

Example:

>>> addr = pyrtl.Input(name="addr", bitwidth=4)
>>> mem = pyrtl.MemBlock(addrwidth=4, bitwidth=8)
>>> read = mem[addr]
>>> read.name = "read"

>>> gate_graph = pyrtl.GateGraph()
>>> read_gate = gate_graph.get_gate("read")
>>> read_gate.mem is mem
True
property memid: int

Readability alias for op_param, returning the MemBlock.id for a read (m) or write (@) op.

Example:

>>> addr = pyrtl.Input(name="addr", bitwidth=4)
>>> mem = pyrtl.MemBlock(addrwidth=4, bitwidth=8)
>>> read = mem[addr]
>>> read.name = "read"

>>> gate_graph = pyrtl.GateGraph()
>>> read_gate = gate_graph.get_gate("read")
>>> read_gate.memid
0
>>> mem.id
0
name: str | None

Name of the operation’s output. Corresponds to WireVector.name.

Some operations do not have outputs, like MemBlock writes. These operations will have a name of None.

Example:

>>> a = pyrtl.Input(name="a", bitwidth=1)
>>> b = pyrtl.Input(name="b", bitwidth=1)
>>> ab = a + b
>>> ab.name = "ab"

>>> gate_graph = pyrtl.GateGraph()
>>> ab_gate = gate_graph.get_gate("ab")
>>> ab_gate.name
'ab'
op: str

Operation performed by this Gate. Corresponds to LogicNet.op.

For special Gates created for Inputs, op will instead be the Input’s _code, which is I.

For special Gates created for Consts, op will instead be the Const’s _code, which is C.

See LogicNet’s documentation for a description of all other ops.

Example:

>>> a = pyrtl.Input(name="a", bitwidth=1)
>>> _ = ~a

>>> gate_graph = pyrtl.GateGraph()
>>> gate_a = gate_graph.get_gate("a")
>>> gate_a.op
'I'
>>> gate_a.dests[0].op
'~'
op_param: tuple

Static parameters for the operation. Corresponds to LogicNet.op_param.

These are constant parameters, whose values are statically known. These values generally do not appear as actual values on wires. For example, the bits to select for the s bit-slice operation are stored as op_params.

Note

Consider using the aliases const_value, reset_value, sel, memid, mem instead, which improve code readability.

Example:

>>> a = pyrtl.Input(name="a", bitwidth=8)
>>> bit_slice = a[1:3]
>>> bit_slice.name = "bit_slice"

>>> gate_graph = pyrtl.GateGraph()
>>> bit_slice_gate = gate_graph.get_gate("bit_slice")
>>> bit_slice_gate.op_param
(1, 2)
property reset_value: int

Readability alias for op_param, returning the reset value for a Register (r) op.

Example:

>>> counter = pyrtl.Register(name="counter", bitwidth=8, reset_value=42)
>>> counter.next <<= counter + 1

>>> gate_graph = pyrtl.GateGraph()
>>> counter_gate = gate_graph.get_gate("counter")
>>> counter_gate.reset_value
42
property sel: tuple[int]

Readability alias for op_param, returning the bits selected by a bit-slice (s) op.

Example:

>>> a = pyrtl.Input(name="a", bitwidth=8)
>>> bit_slice = a[2:6]
>>> bit_slice.name = "bit_slice"

>>> gate_graph = pyrtl.GateGraph()
>>> bit_slice_gate = gate_graph.get_gate("bit_slice")
>>> bit_slice_gate.sel
(2, 3, 4, 5)
class pyrtl.GateGraph(block=None)[source]

A GateGraph is a collection of Gates. GateGraph’s constructor creates Gates from a Block.

See Motivation for more background.

Users should generally construct GateGraphs, rather than attempting to directly construct individual Gates. Gate construction is complex because they are doubly-linked, and the Gate graph may contain cycles.

Example

Let’s build a GateGraph for the Motivation example:

>>> a = pyrtl.Input(name="a", bitwidth=1)
>>> b = pyrtl.Input(name="b", bitwidth=1)
>>> c = pyrtl.Input(name="c", bitwidth=1)

>>> x = a & b
>>> x.name = "x"

>>> y = x | c
>>> y.name = "y"

>>> gate_graph = pyrtl.GateGraph()

The GateGraph can be printed, revealing five Gates:

>>> print(gate_graph)
a/1 = Input
b/1 = Input
c/1 = Input
x/1 = and(a/1, b/1)
y/1 = or(x/1, c/1)

We can retrieve the Gate for input a:

>>> a = gate_graph.get_gate("a")
>>> print(a)
a/1 = Input
>>> a.name
'a'
>>> a.op
'I'

We can check a’s dests to see that it is an argument to a bitwise & operation, with output named x:

>>> len(a.dests)
1
>>> x = a.dests[0]
>>> print(x)
x/1 = and(a/1, b/1)
>>> x.op
'&'
>>> x.name
'x'

We can examine the bitwise &’s args, to get references to input Gates a and b:

>>> x.args[0] is a
True

>>> b = x.args[1]
>>> print(b)
b/1 = Input
>>> b.name
'b'
>>> b.op
'I'

Special Gates

Generally, GateGraph converts each LogicNet in a Block to a corresponding Gate, but some WireVectors and LogicNets are handled differently:

Note

Registers can create cycles in the Gate graph, because the logic that defines the register’s next value (which is the register Gate’s args) can depend on the register’s current value (which is the register Gate’s dests). Watch out for infinite loops when traversing a GateGraph with registers. For example, if you keep following dests references, you may end up back where you started.

__init__(block=None)[source]

Create Gates from a Block.

Most users should call this constructor, rather than attempting to directly construct individual Gates.

Parameters:

block (Block, default: None) – Block to construct the GateGraph from. Defaults to the working_block.

__iter__()[source]

Iterate over each gate in the GateGraph.

Similar to gates, except that __iter__ returns an Iterable rather than a set.

Example:

>>> a = pyrtl.Input(name="a", bitwidth=2)
>>> b = pyrtl.Input(name="b", bitwidth=2)
>>> sum = a + b
>>> sum.name = "sum"

>>> gate_graph = pyrtl.GateGraph()

>>> sorted(gate.name for gate in gate_graph)
['a', 'b', 'sum']
__str__()[source]

Return a string representation of the GateGraph.

Example:

>>> a = pyrtl.Input(name="a", bitwidth=2)
>>> b = pyrtl.Input(name="b", bitwidth=2)
>>> sum = a + b
>>> sum.name = "sum"

>>> gate_graph = pyrtl.GateGraph()

>>> print(gate_graph)
a/2 = Input
b/2 = Input
sum/3 = add(a/2, b/2)
Return type:

str

Returns:

A string representation of each Gate in the GateGraph, one Gate per line. The Gates will be sorted by name.

consts: set[Gate]

A set of Const Gates in the GateGraph.

These Gates provide constant values, with op C.

Example:

>>> c = pyrtl.Const(name="c", val=0)
>>> d = pyrtl.Const(name="d", val=1)
>>> _ = c + d

>>> gate_graph = pyrtl.GateGraph()

>>> sorted(gate.name for gate in gate_graph.consts)
['c', 'd']
gates: set[Gate]

A set of all Gates in the GateGraph.

Similar to __iter__, except that gates is a set rather than an Iterable.

Example:

>>> a = pyrtl.Input(name="a", bitwidth=1)
>>> b = pyrtl.Input(name="b", bitwidth=1)
>>> c = pyrtl.Input(name="c", bitwidth=1)
>>> x = a & b
>>> x.name = "x"
>>> y = x | c
>>> y.name = "y"

>>> gate_graph = pyrtl.GateGraph()

>>> sorted(gate.name for gate in gate_graph.gates)
['a', 'b', 'c', 'x', 'y']
get_gate(name)[source]

Return the Gate whose name is name, or None if no such Gate exists.

Warning

MemBlock writes do not produce an output, so they can not be retrieved with get_gate.

Example:

>>> a = pyrtl.Input(name="a", bitwidth=1)
>>> na = ~a
>>> na.name = "na"

>>> gate_graph = pyrtl.GateGraph()

>>> a_gate = gate_graph.get_gate("a")
>>> na_gate = gate_graph.get_gate("na")
>>> na_gate.op
'~'
>>> na_gate.args[0] is a_gate
True
Parameters:

name (str) – Name of the Gate to find.

Return type:

Gate | None

Returns:

The named Gate, or None if no such Gate was found.

inputs: set[Gate]

A set of Input Gates in the GateGraph.

These Gates provide Input values, with op I.

Example:

>>> a = pyrtl.Input(name="a", bitwidth=1)
>>> b = pyrtl.Input(name="b", bitwidth=1)
>>> _ = a & b

>>> gate_graph = pyrtl.GateGraph()

>>> sorted(gate.name for gate in gate_graph.inputs)
['a', 'b']
mem_reads: set[Gate]

A set of MemBlock read Gates in the GateGraph.

These Gates read MemBlocks, with op m.

Example:

>>> mem = pyrtl.MemBlock(name="mem", bitwidth=4, addrwidth=2)
>>> addr = pyrtl.Input(name="addr", bitwidth=2)
>>> mem_read_1 = mem[addr]
>>> mem_read_1.name = "mem_read_1"
>>> mem_read_2 = mem[addr]
>>> mem_read_2.name = "mem_read_2"

>>> gate_graph = pyrtl.GateGraph()

>>> sorted(gate.name for gate in gate_graph.reads)
['mem_read_1', 'mem_read_2']
mem_writes: set[Gate]

A set of MemBlock write Gates in the GateGraph.

These Gates write MemBlocks, with op @.

Example:

>>> mem = pyrtl.MemBlock(name="mem", bitwidth=4, addrwidth=2)
>>> addr = pyrtl.Input(name="addr", bitwidth=2)
>>> mem[addr] <<= 7

>>> gate_graph = pyrtl.GateGraph()

>>> # MemBlock writes have no name.
>>> [gate.name for gate in gate_graph.mem_writes]
[None]

>>> [gate.op for gate in gate_graph.mem_writes]
['@']
outputs: set[Gate]

A set of Output Gates in the GateGraph.

These Gates set Output values, with is_output True.

Example:

>>> x = pyrtl.Output(name="x")
>>> y = pyrtl.Output(name="y")
>>> x <<= 42
>>> y <<= 255

>>> gate_graph = pyrtl.GateGraph()

>>> sorted(gate.name for gate in gate_graph.outputs)
['x', 'y']
registers: set[Gate]

A set of Register update Gates in the GateGraph.

These Gates set a Register’s value for the next cycle, with op r.

Example:

>>> r = pyrtl.Register(name="r", bitwidth=1)
>>> s = pyrtl.Register(name="s", bitwidth=1)
>>> r.next <<= r + 1
>>> s.next <<= s + 2

>>> gate_graph = pyrtl.GateGraph()

>>> sorted(gate.name for gate in gate_graph.registers)
['r', 's']
sinks: set[Gate]

A set of sink Gates in the GateGraph.

A sink Gate’s output value is known only at the end of each clock cycle. Registers, Outputs, MemBlock writes, and any Gate without users (len(dests) == 0) are sink Gates.

Note

Registers are both sources and sinks. As a source, it provides the Register’s value for the current cycle. As a sink, it determines the Register’s value for the next cycle.

Example:

>>> a = pyrtl.Input(name="a", bitwidth=1)
>>> r = pyrtl.Register(name="r", bitwidth=1)
>>> o = pyrtl.Output(name="o", bitwidth=1)
>>> r.next <<= a + 1
>>> o <<= 1
>>> sum = a + r
>>> sum.name = "sum"

>>> gate_graph = pyrtl.GateGraph()

>>> sorted(gate.name for gate in gate_graph.sinks)
['o', 'r', 'sum']
sources: set[Gate]

A set of source Gates in the GateGraph.

A source Gate’s output value is known at the beginning of each clock cycle. Consts, Inputs, and Registers are source Gates.

Note

Registers are both sources and sinks. As a source, it provides the Register’s value for the current cycle. As a sink, it determines the Register’s value for the next cycle.

Example:

>>> a = pyrtl.Input(name="a", bitwidth=1)
>>> c = pyrtl.Const(name="c", bitwidth=1, val=0)
>>> r = pyrtl.Register(name="r", bitwidth=1)
>>> r.next <<= a + c

>>> gate_graph = pyrtl.GateGraph()

>>> sorted(gate.name for gate in gate_graph.sources)
['a', 'c', 'r']