Skip to content

Decorators

julianspeith edited this page Sep 1, 2026 · 10 revisions

Decorators wrap a netlist, a net, or a Boolean function and add higher-level operations to it. They exist to keep the core netlist classes small: Gate and Net only know about their immediate neighbors, while anything that involves walking further, reasoning symbolically, or restructuring the netlist lives in a decorator.

In practice this is where much of HAL's actual analytical power sits, so it is worth knowing what is available before writing your own traversal loop.

Decorators are instantiated around the object they operate on and then used like ordinary objects:

trav = hal_py.NetlistTraversalDecorator(netlist)
sub = hal_py.SubgraphNetlistDecorator(netlist)

Most decorator functions return None on failure and log the reason to the log, so check the return value before using it.

Netlist traversal decorator

NetlistTraversalDecorator(netlist) answers the question "what do I reach from here?" — the operation you perform constantly when following a signal through a design. Its central idea is that traversal should stop at meaningful gates rather than at every gate, so that the combinational logic between two registers does not obscure the structure you are looking for.

get_next_sequential_gates

Starting from a gate or net, return the next layer of sequential gates reachable through combinational logic. This is the standard way to build a register-level view of a design: applying it to every flip-flop yields the graph on which register grouping and datapath recovery operate.

The forbidden_pins argument is what makes it precise. Passing {hal_py.PinType.clock} prevents the traversal from escaping through clock pins, so you follow data dependencies and do not end up with every flip-flop in the design connected to every other one through the clock tree.

trav = hal_py.NetlistTraversalDecorator(netlist)
next_ffs = trav.get_next_sequential_gates(ff, True, {hal_py.PinType.clock})

get_next_sequential_gates_map(successors, forbidden_pins) computes this for all sequential gates in the netlist at once and returns a dict from gate to its set of successors (or predecessors). Use it when you need the full register graph — it is much faster than calling the single-gate variant in a loop.

reg_graph = trav.get_next_sequential_gates_map(True, {hal_py.PinType.clock})

get_combinational_cone

Return the combinational cone of a gate or net — its combinational fan-out (successors=True) or fan-in (successors=False), extending up to the sequential boundary — again with a set of forbidden pin types. This is the set of gates you would hand to the subgraph decorator to obtain a Boolean function.

cone = trav.get_combinational_cone(net, False, {hal_py.PinType.clock})

get_gates — the traversal underneath

Every traversal above is one and the same walk, differing only in where it stops relative to the gates it is looking for. get_gates says that out loud instead of implying it through a pair of booleans:

# the next MUX gates reachable from this gate, not passing through clock pins
muxes = trav.get_gates(
    gate, hal_py.TraversalDirection.forward,
    lambda g: g.get_type().has_property(hal_py.GateTypeProperty.c_mux),
    hal_py.TraversalStop.at_match,
    entry_endpoint_filter=lambda ep, depth: ep.get_pin().get_type() != hal_py.PinType.clock)

The match condition says what to collect; TraversalStop says where the walk halts:

TraversalStop meaning the collected gates are
at_match stop at a gate the condition accepts the boundary of the search — e.g. the next flip-flops behind a cone of logic
at_mismatch stop at a gate the condition rejects one connected region of matches — e.g. the combinational logic between registers
never do not stop at gates at all everything matching within reach; bound this with max_depth or the endpoint filters

TraversalDirection is forward (fan-out), backward (fan-in) or both, which returns the union of the two. max_depth bounds the walk, and the endpoint filters decide whether it may leave or enter a gate through a given endpoint.

get_next_sequential_gates is get_gates with the condition pinned to sequential gates and at_match; get_combinational_cone pins combinational and at_mismatch. The older general forms remain — get_next_matching_gates (stops at matches), get_next_matching_gates_until (stops at mismatches, despite the name), get_next_matching_gates_until_depth (never stops, depth-bounded) — but get_gates is the one whose call sites read correctly.

Caching repeated traversals

Asking the same kind of question for many start points — every flip-flop of a design, say — re-walks the same logic over and over. make_traversal_cache creates a store for one specific traversal: the direction, condition, stop rule and endpoint filters are sealed in at creation, and results are shared across every call using the cache.

is_seq = lambda g: g.get_type().has_property(hal_py.GateTypeProperty.sequential)
cache = trav.make_traversal_cache(hal_py.TraversalDirection.forward, is_seq, hal_py.TraversalStop.at_match)
for ff in flip_flops:
    next_ffs = trav.get_gates(ff, cache)

A cache can only be used for exactly the traversal it was created for and only on the netlist it was created for — anything else is refused rather than silently answered wrong. Drop it when the netlist is modified. There is deliberately no depth limit and the filters receive no depth: either would make a cached answer depend on how a net was reached, which is precisely what a cache must not do.

get_shortest_path and get_shortest_path_distance

Find the shortest path between two gates, from a gate to any gate of a module, or every shortest path between two modules — or just the length. All accept endpoint filters, so you can ask for the shortest path that does not pass through, say, reset logic.

path  = trav.get_shortest_path(start_gate, end_gate, hal_py.PinDirection.output)     # inout searches both ways
paths = trav.get_shortest_path(module_a, module_b, hal_py.PinDirection.output)       # all tied-shortest paths
dist  = trav.get_shortest_path_distance(start_gate, end_gate, hal_py.PinDirection.output)

A start gate that already belongs to the target module yields a path of just that gate, which tells "already there" apart from "unreachable" (an empty result).

get_gate_chain and get_complex_gate_chain

Starting from a gate, follow a chain of identically-typed gates connected through the specified input and output pins, and return all gates in that chain. Carry chains and shift registers are the classic targets: finding them is a strong signal about word-level structure, since a carry chain implies an adder and its ordering reveals the bit order of the operands.

chain = trav.get_gate_chain(start_gate)                       # any pins
chain = trav.get_gate_chain(start_gate, [ci_pin], [co_pin])   # via specific pins

get_complex_gate_chain is the same idea for chains built from a repeating sequence of gate types rather than a single type — for example a chain alternating between a LUT and a carry gate, as produced by FPGA synthesis for arithmetic. An optional filter on both further restricts which gates may join the chain.

get_common_inputs

Given a set of gates, return the nets that feed at least threshold of them (0 requires all; nets driven by GND or VCC do not count). Shared inputs across a group of gates typically indicate a shared control signal — a clock enable, a select line, a round constant — and hence that the gates belong together. This makes it a cheap way to test whether a candidate group of gates really forms one functional unit.

shared = trav.get_common_inputs(gates, threshold=len(gates) // 2)

Subgraph netlist decorator

It is often desirable to generate a Boolean function that is made up of multiple interconnected combinational gates. For this purpose, instantiate a SubgraphNetlistDecorator on the netlist and use its get_subgraph_function method. It takes a module or a list of gates plus the output net whose function you want, and recursively composes the Boolean functions of all combinational gates in between. See the respective Python documentation.

The recursion terminates when it reaches either a gate outside of the provided subgraph or a non-combinational gate. The variables of the resulting function are named net_[ID] after the nets entering the subgraph, since gate pin names are not unique across gates.

sub = hal_py.SubgraphNetlistDecorator(netlist)
bf = sub.get_subgraph_function(top_module.get_gates(), output_net)

An overload accepts a cache dict. Pass the same cache across repeated calls — for instance when extracting one function per output bit of a multi-bit port — to avoid recomputing shared logic:

cache = {}
funcs = [sub.get_subgraph_function(gates, pin.get_net(), cache) for pin in grp.get_pins()]

get_subgraph_function_inputs returns just the set of nets the function would depend on, without building the function itself. That is much cheaper, and enough when you only want to know what a subcircuit reads.

copy_subgraph_netlist extracts a set of gates (or a module) into a new, standalone netlist with its own global inputs and outputs. This is how you isolate a recovered component so it can be simulated, exported, or analyzed independently of the surrounding design.

sub_nl = sub.copy_subgraph_netlist(module)

The Simple ALU example project walks through using this decorator end to end.

Boolean function decorator

BooleanFunctionDecorator(bf) and its static helpers bridge between the netlist and the symbolic world of Boolean functions.

get_boolean_function_from is the important one: it concatenates several single-bit items into one multi-bit function. It accepts a list of Boolean functions, a list of nets, or a module pin group — the last being the most convenient, since it takes a whole A or KEY port and turns it into a single word-level variable. Optional arguments control zero- or sign-extension (extend_to_size, sign_extend) and bit order (ascending).

grp_a = top_module.get_pin_group_by_name("A")
var_a = hal_py.BooleanFunctionDecorator.get_boolean_function_from(grp_a)

This is what makes word-level reasoning possible: once inputs and outputs are 8- or 128-bit variables, you can state properties like "this circuit computes A + B" and hand them to an SMT solver.

substitute_power_ground_nets(nl) and substitute_power_ground_pins(g) replace variables corresponding to constant nets or power/ground pins with the constants 0 and 1. Applying this before simplification often collapses a function dramatically, because tie-offs are extremely common in synthesized netlists.

Boolean function net decorator

BooleanFunctionNetDecorator(net) translates between nets and the variables that represent them. get_boolean_variable returns the variable for a net and get_boolean_variable_name its name, while the static get_net_from(netlist, var) goes back the other way.

var = hal_py.BooleanFunctionNetDecorator(net).get_boolean_variable()
net = hal_py.BooleanFunctionNetDecorator.get_net_from(netlist, var)

Use this instead of parsing net_[ID] strings yourself. It is the supported way to map a solver result or a simplified function back onto concrete nets in the design.

Netlist modification decorator

NetlistModificationDecorator(netlist) collects operations that restructure a netlist while keeping it consistent.

  • connect_gates(src_gate, src_pin, dst_gate, dst_pin) connects an output pin of one gate to an input pin of another, creating or reusing a net as needed.
  • connect_nets(master_net, slave_net) merges two nets into one, moving all sources and destinations of the slave onto the master.
  • replace_gate(gate, target_type, pin_map) replaces a gate with one of a different gate type, reconnecting pins according to the map.
  • delete_modules(filter) deletes all modules matching a filter, handing their gates to the respective parent. Useful for flattening a hierarchy that turned out to be misleading.
mod = hal_py.NetlistModificationDecorator(netlist)
mod.connect_gates(driver, "O", receiver, "A")
mod.delete_modules(lambda m: m.get_name().startswith("dana_"))

Prefer these functions over manual create_net / add_source / add_destination sequences: they perform the consistency checks that hand-rolled rewiring tends to skip.

See also

  • Netlist Utilities — free functions covering ground the decorators do not
  • Boolean Function — what the subgraph and Boolean function decorators produce
  • SMT Solving — the usual next step once a subgraph function has been extracted
  • Netlist — the netlist that gets decorated

Clone this wiki locally