slang-netlist  0.9.0
Loading...
Searching...
No Matches
User guide

Getting started

Currently there are no pre-built binaries, so you will need to build from source.

Make sure you have the following prerequisites:

  • CMake >= 3.20
  • Python 3
  • C++20 compiler

Then:

git clone https://github.com/jameshanlon/slang-netlist.git
cd slang-netlist
cmake -B build \
-DCMAKE_BUILD_TYPE=Release \
-DENABLE_PY_BINDINGS=ON \
-DCMAKE_INSTALL_PREFIX=$PWD/install
cmake --build build -j --target install
ctest --test-dir build

When using the Python bindings, it is recommended to use the pyslang shared object file that is produced as part of the build. Different versions of the upstream slang Python bindings may not work.

CLI usage

There are two command-line tools available: slang-netlist exposes the netlist-graph building and querying functions of the library; and slang-report provides extra AST-based reporting functions beyond the standard slang driver. Both tools accept SystemVerilog source files and the standard slang compilation options. Run either with --help for the full option list.

General options (slang-netlist)

  • -h, --help — display available options.
  • --version — print version information and exit.
  • --no-colours — disable coloured output.
  • -q, --quiet — suppress non-essential output.
  • -d, --debug — enable debug logging (debug builds only).
  • -j, --threads <N> — set the number of threads (0 = hardware concurrency, 1 = sequential).
  • --stats — print phase timings and peak memory to stderr.
  • --stats-json — print phase timings and peak memory in JSON format to stdout.
  • --no-resolve-assign-bits — disable bit-aligned dependency resolution of concatenations, replications, conversions, and equal-width conditional operators in assignments and port connections; see Bit dependency resolution.
  • --no-prop-cuts-across-ports — disable propagation of concat-induced cut points across module port boundaries. With this set, port nodes and module-internal assignments stay whole-word at port boundaries and paths through concatenated ports become bit-imprecise; see Bit dependency resolution.
  • --black-box <pattern> — treat instances whose definition name or hierarchical path matches the wildcard pattern (* and ?) as black boxes. Black-boxed instances get port nodes and external wiring, but their body is not traversed, so paths terminate at the boundary. The option may be repeated to supply multiple patterns.

Query commands

--from <name> --to <name> — find a path between two named points. Names are hierarchical paths such as top.sub.signal. The path is printed with source locations when available.

slang-netlist design.sv --from top.a --to top.b

A path query prints an alternating sequence of nodes and edges from the start signal to the end signal. Each step shows a source location and a description:

  • "input port" / "output port" — a module port boundary.
  • "assignment" — a continuous or procedural assignment.
  • "conditional statement" / "case statement" — a branch in a procedural block.
  • "value" — the edge between two nodes, annotated with the driven signal name and bit range.

When the design has been compiled with the slang AST available, source locations are available and the output includes annotated source lines; otherwise after loading a serialised netlist, a plain file:line:col format is used.

--fan-out <name> — report the combinational fan-out cone from a named node (all nodes reachable via combinational edges, stopping at registers).

--fan-in <name> — report the combinational fan-in cone to a named node.

--find <pattern> — find named nodes matching a wildcard pattern (using * and ? characters to match any substring and any character respectively).

--find-regex <pattern> — find named nodes matching a regex pattern.

Reports

--report-registers — list all registers (State nodes) in the design. A State node is created for any variable that is assigned inside a clocked procedural block (always_ff or an always block with an edge-sensitive event list). Each entry shows the hierarchical path and source location.

--comb-loops — detect and report combinational loops. Reports cycles in the graph that consist entirely of combinational (non-clocked) edges. Each loop is printed as a path from a node back to itself. A reported loop means the tool found a structural cycle in the source-level dependency graph; see Bit dependency resolution for caveats on precision.

For AST-level reports (variables, ports, drivers, AST JSON dump) use slang-report — see slang-report below.

Export and serialization

--netlist-dot <file> — write the netlist graph in DOT format. Use - for stdout. The output contains one graph node per NetlistNode and one graph edge per NetlistEdge. Node labels show the node kind and hierarchical path where applicable; edge labels show the driven signal and bit range. The DOT output can be rendered with Graphviz tools such as dot, neato, or fdp:

slang-netlist design.sv --netlist-dot - | dot -Tsvg -o netlist.svg

--save-netlist <file> — serialize the netlist graph to a JSON file for later reloading.

--load-netlist <file> — load a previously saved netlist (skips compilation). Analysis commands such as --from/--to, --comb-loops, and --report-registers work on loaded netlists.

slang-report

slang-report runs reports against the elaborated AST without building the netlist graph. It accepts the standard slang compilation options plus one of the following actions:

  • --ports — list all ports in the design (hierarchical path, direction, source location).
  • --variables — list all variables in the design (hierarchical path, source location).
  • --drivers — list all drivers for each value symbol. Runs slang's analysis pass; each entry shows the driving expression, bit range, driver kind (procedural or continuous), and source location.
  • --ast-json <file> — dump the compiled AST in JSON format. Use - for stdout. Restrict the dump to specific scopes with --ast-json-scope <path> (may be repeated).
slang-report design.sv --ports
slang-report design.sv --drivers
slang-report design.sv --ast-json design.ast.json

Python bindings

The pyslang_netlist module (enabled with -DENABLE_PY_BINDINGS=ON) exposes the graph data structures and analysis routines to Python. It depends on pyslang for the compilation and analysis types.

Building a graph

The build pipeline has four steps: force lazy AST construction with VisitAll, freeze the compilation, run AnalysisManager, then unfreeze the compilation so NetlistGraph.build can keep elaborating.

import pyslang
import pyslang_netlist
# Compile.
tree = pyslang.syntax.SyntaxTree.fromText("""
module m(input logic a, output logic b);
assign b = a;
endmodule
""")
compilation = pyslang.ast.Compilation()
compilation.addSyntaxTree(tree)
# Force construction of the whole AST before freezing, since AST
# construction is lazy and visiting a previously-unvisited node can
# mutate the compilation (which is not thread-safe).
pyslang_netlist.VisitAll().run(compilation)
compilation.freeze()
# Run analysis.
am = pyslang.analysis.AnalysisManager()
am.analyze(compilation)
# Unfreeze before building: the netlist builder needs to keep
# elaborating the AST.
compilation.unfreeze()
# Build the netlist.
graph = pyslang_netlist.NetlistGraph()
graph.build(compilation, am)

NetlistGraph.build accepts these keyword arguments:

  • parallel (default True) — dispatch DFA work and R-value resolution across a thread pool.
  • num_threads (default 0) — thread pool size; 0 uses hardware concurrency.
  • resolve_assign_bits (default True) — bit-aligned dependency resolution; see Bit dependency resolution.
  • prop_cuts_across_ports (default True) — propagate concat-induced cut points across port boundaries; see Bit dependency resolution.
  • black_boxes (default []) — list of glob patterns (*, ?) matched against each instance's definition name and hierarchical path. Matched instances skip body traversal and record only port-boundary connectivity.

Querying the graph

# Graph size.
print(graph.num_nodes(), graph.num_edges())
# Lookup a node by hierarchical name.
node = graph.lookup("m.a") # Returns NetlistNode or None
# Lookup nodes by name and bit range overlap.
nodes = graph.lookup_by_range("m.a", 0, 7)
# Find nodes by wildcard or regex.
nodes = graph.find_nodes("m.*")
nodes = graph.find_nodes_regex(r"m\.[ab]")
# Get drivers of a signal over a bit range.
drivers = graph.get_drivers("m.b", 0, 0)
# Combinational fan-out / fan-in (stops at State nodes).
fan_out = graph.get_comb_fan_out(node)
fan_in = graph.get_comb_fan_in(node)

Path finding

finder = pyslang_netlist.PathFinder()
# Find any path.
path = finder.find(start_node, end_node)
# Find a combinational-only path (skips State nodes).
path = finder.find_comb(start_node, end_node)
if not path.empty():
for node in path:
print(node.kind, getattr(node, "path", ""))

Iterating over nodes

# Iterate all nodes.
for node in graph:
print(node.ID, node.kind)
# Access typed properties on specific node kinds.
if node.kind == pyslang_netlist.NodeKind.Port:
port = node # Port subclass
print(port.name, port.path, port.bounds, port.is_input())

Available node types and their properties:

  • Portname, path, direction, bounds, is_input(), is_output(), is_driven()
  • Variablename, path, bounds
  • Statename, path, bounds
  • Constantwidth, value (string form of the constant value). Created for literal RHSs, constant-foldable expressions, zero-extension padding bits, literal port connections, and constant arms of conditional operators.
  • Assignment, Conditional, Case, Merge — no additional properties

The bounds property on nodes and edges is a DriverBitRange with lower, upper, and width accessors; it also unpacks as a (lower, upper) tuple.

Edge properties: symbol_name, symbol_path, bounds, disabled.

Performance tuning

Threading

Use -j <N> to set the number of threads. The default (-j 0) uses hardware concurrency. For small designs the overhead of thread management can outweigh the benefit; -j 1 forces sequential execution.

The parallel phases of construction are Phase 2 (DFA dispatch) and Phase 4 (R-value resolution). Phase 2 dispatches one task per procedural block and continuous assignment, so designs with many independent blocks benefit most from additional threads. Phase 4 only parallelises when the number of pending R-values exceeds an internal threshold (1000 by default).

Profiling

Use --stats to print a human-readable timing breakdown to stderr:

slang-netlist design.sv --report-registers --stats

The output shows per-phase timings (collect, parallel DFA, drain, resolve R-values), per-task statistics (min/max/mean/median), and peak RSS. Use --stats-json for machine-readable JSON output.

Considerations

There are various factors that affect performance and how much multithreading improves this. Here are some to consider; note that the execution phases of the netlist construction are detailed in Graph construction.

  • Design size. More ports, variables and instances means more nodes and edges to create in an sequential traversal of the AST (Phase 1).
  • Procedural block count. Each always / initial block is a separate DFA task in Phase 2; designs with many small blocks parallelise well, while designs dominated by a few large blocks may be limited by the longest task.
  • R-value count. Complex expressions with many operands generate more pending R-values for Phase 4 to resolve.
  • Hierarchy depth: deeply nested module instantiations increase the AST traversal time and the number of port connections to process.

Analysis model

Slang Netlist builds its dependency graph by analysing the SystemVerilog source directly, without performing logic synthesis. This makes it fast, portable across target technologies, and able to operate on partial or non-synthesisable designs. However, because the tool does not flatten the design down to a gate- or LUT-level netlist, the analysis is necessarily more conservative than what a synthesis tool could provide. The key differences are described below.

No-synthesis elaboration

A synthesis tool resolves every operator, generate construct, and hierarchical reference into a flat network of primitive gates or technology cells. Slang Netlist instead works at the SystemVerilog source level: it preserves the original module hierarchy, treats each assignment as an atomic operation and records data dependencies between the variables that appear on each side. This means the graph is a faithful representation of the source structure, but it does not always capture the Boolean-level logic that a synthesised netlist would expose.

Bit dependency resolution

When an assignment connects symbols on its left- and right-hand sides, the graph records dependencies bit-by-bit wherever the source structure makes the per-bit correspondence clear. Concatenations, replications, and width-changing conversions are tracked precisely, so the bits of one symbol are connected only to the bits they actually drive on the other side:

assign {a, b} = {x, y}; // x connects to a, y connects to b
assign w = {2{p}}; // p connects to both halves of w
assign z[31:0] = q[15:0]; // only the lower 16 bits of z have a driver;
// the upper 16 are zero-extended

This holds across the structural rearrangements you can write in SystemVerilog: nesting concatenations, replicating with a constant count, widening to a larger type (the extra bits are recorded as zero- or sign-extension and have no driver), and using a conditional operator ?: where both arms have the same width as the result (the condition itself becomes an extra dependency on every bit).

The same rules apply to port connections, which are treated as assignments. A hookup such as .in({x,y}) on a two-bit input port connects x and y to the matching individual bits of the port.

Bit precision also carries through the port itself. When a concat appears on a port connection, the cut points are propagated inward so that the formal port nodes and the module-internal assignments are split at the same bit boundaries. For example, given:

module sub(input logic [1:0] x, output logic [1:0] y);
assign y = x;
endmodule
module top(input logic a, b, output logic c, d);
sub u (.x({b, a}), .y({d, c}));
endmodule

The graph reports a→c and b→d but no cross-bit paths.

Known limitations
  • Operators are not analysed bit-by-bit. As soon as an expression contains arithmetic (+, -, *, …), a bitwise or relational operator, a reduction, a function call, a streaming concatenation, a non-constant select, a narrowing conversion, or a pattern-bearing conditional, every symbol read inside that expression is treated as contributing to every bit the expression produces. So in y = a & b the graph still records that every bit of y depends on every bit of a and b, even though & is bitwise in nature.
  • Bit-aligned resolution only applies to integral types whose widths agree on both sides. Strings, unpacked aggregates, and dynamically-sized types, along with assignments where the two sides have different selectable widths (for example an enum coerced to a logic at a port boundary), fall back to a coarser per-symbol connection.
Fallback options

Bit-aligned resolution can be turned off with --no-resolve-assign-bits on the slang-netlist CLI, or by passing resolve_assign_bits=False to NetlistGraph.build in the Python bindings. With it off, every symbol on one side of an assignment is connected to every symbol on the other side, matching the behaviour of releases before bit-aligned resolution landed.

Cross-port cut propagation can be turned off with --no-prop-cuts-across-ports on the slang-netlist CLI or by passing prop_cuts_across_ports=False to NetlistGraph.build in the Python bindings. With it off, port nodes and module-internal assignments stay whole-word at port boundaries even when the actual side of the connection is a concat.

In every fallback case the resulting graph is still sound (no real connection is missed) but path and dependency queries over the affected symbols may report paths that a deeper Boolean analysis could rule out.

Black-boxed instances

Instances can be black-boxed to stop the netlist builder from descending into their bodies. A black-boxed instance still receives port nodes and external port-connection wiring, but its internal assignments and sub-instances are not traversed, so fan-in/fan-out paths terminate at the port boundary. This is useful for excluding vendor IP, technology cells, or any sub-tree whose internals are irrelevant to the analysis at hand.

Patterns are glob expressions (* and ?) and are matched against either an instance's definition name or its hierarchical instance path, so the same pattern can target a single instance or every instantiation of a module:

slang-netlist design.sv --black-box ram_* --black-box top.cpu.fpu

The Python equivalent passes a list of patterns to NetlistGraph.build:

graph.build(compilation, am, black_boxes=["ram_*", "top.cpu.fpu"])

SystemVerilog support

The following SystemVerilog constructs are handled during netlist construction:

  • Module and interface hierarchies, including generate blocks and arrays of instances.
  • Input, output, and inout ports; port connections (named and positional).
  • Interface modport ports and their connection expressions.
  • Continuous assignments (assign).
  • Procedural blocks: always, always_ff, always_comb, initial.
  • Procedural assignments (blocking = and non-blocking <=).
  • Conditional statements (if/else) and case statements within procedural blocks, including static constant folding of unreachable branches.
  • Bit and range selects on the left-hand and right-hand sides of assignments.
  • Multi-dimensional arrays and packed/unpacked struct member access.

SystemVerilog constructs that are not currently supported or have limited handling:

  • Tasks and functions — calls are not followed; dependencies through function arguments are not tracked.
  • Assertions and coverage constructs are ignored.
  • System tasks and functions ($display, etc.) are ignored.
  • force/release and deassign are not modelled.
  • Dynamic types, classes, and constraint blocks are not modelled.
  • Cross-module references (XMRs) are partially supported through slang's hierarchical reference resolution.

Potential use cases

Here are some ideas for ways in which Slang Netlist can be used in digital design.

  • Connectivity checks. Testing for the existence of paths can be used to implement lightweight lint checks for design structure. For example, checking that a control signal fans out correctly to all memories in a design. Equally, the absence of paths can be also be checked, for example to assert that design components are correctly isolated.
  • Static timing / critical-path estimation. By annotating edges with a simple unit-delay or gate-type-based delay model, the longest combinational path between any two register stages could be reported. Even without real timing analysis, logic depth is a useful early-stage proxy for critical paths and helps flag modules that will struggle to meet frequency targets.
  • Clock domain crossing checks (CDC). This requires tracing connectivity across register boundaries and correlating with clock edges. Slang knows about EdgeKind but doesn't do cross-domain path analysis.
  • Reset domain crossing checks (RDC). Tracing which registers share reset paths and flagging cross-domain reset issues. Similar to CDC but for resets.
  • Cone-of-influence extraction. Given a target signal, extract the minimal sub-design (all transitive fan-in) needed to compute it. This could output a reduced SV module or a sub-graph DOT file, useful for debugging or feeding a smaller design into formal verification tools.
  • Netlist diffing. Compare netlists from two revisions of the same design and report structural differences: added/removed signals, changed connectivity, altered fan-in/fan-out. This would be valuable to catch unintended RTL changes, as a lightweight netlist equivalence check.
  • Hierarchical complexity metrics. Report per-module statistics such as node count, edge count, combinational depth, fan-out distribution, register count. These metrics could help identify overly complex modules that should be refactored and can track design complexity over time.