When we started building CADY, the core question was how to represent a schematic in a form that made rule evaluation fast and reliable. A schematic is not a single flat structure: it has hierarchical sheets, global and local power symbols, port connections between sheets, bus definitions, and component properties that vary in format depending on the EDA tool that produced the export. Translating that into something a rule engine can reason about cleanly required explicit decisions at every stage of the pipeline.
This post walks through how CADY parses netlist files, resolves the net table, builds the component graph, and evaluates predicate rules against it. The goal is to explain the design choices clearly enough that you can reason about what CADY can and cannot check, and why.
Parsing: from EDA export to intermediate representation
CADY accepts netlist files in three main formats: KiCad's XML netlist (.net), Altium's IPC-2581 output, and a subset of SPICE netlist notation. Each format carries the same core information (component references, pin assignments, net names) but in different structural forms, with different conventions for representing power symbols, hierarchical sheet ports, and custom component properties.
The first parsing stage converts each format into a common intermediate representation (IR). The IR is a flat enumeration of three object types: nets, components, and pins. A net is a named conductor. A component is a placed instance with a reference designator, value, footprint reference, and a set of component properties. A pin is the connection point between a component and a net.
The key decision here was to normalize everything into this flat form before any rule evaluation begins. An alternative approach would be to evaluate rules directly against the parsed tree of each format. We tried this in the initial prototype and abandoned it because it meant every rule had to account for format-specific structural variations. Normalizing to IR first means rules are written against a single clean model.
Net resolution: from names to conductors
Raw netlist exports contain net names, not net identities. The same electrical conductor can appear under multiple names depending on how it crosses sheet boundaries. In KiCad hierarchical schematics, a net named SPI_MOSI on one sheet may appear as /MCU_SHEET/SPI_MOSI in the flat netlist output. Power symbols like +3V3 and VCC may refer to the same conductor depending on how they are connected in the schematic, or they may be intentionally separate.
Net resolution is the process of building a canonical conductor table that assigns a unique identity to each electrically distinct conductor, regardless of how many names it carries in the raw export. We implement this with a union-find algorithm over the pin connections: two nets that are connected through a non-isolating component (a wire junction, a direct pin-to-pin connection, or a zero-ohm resistor if its reference is in the passive bridge list) are merged into a single conductor. Power symbols with the same value on the same sheet are merged by convention unless a deliberate break is present.
The net resolution step is where we see the most format-specific edge cases. Altium's IPC-2581 output handles hierarchical net references differently from KiCad, and SPICE netlists have their own conventions for global power nodes. We maintain format-specific normalization passes before the union-find step to handle these.
Component graph construction
After net resolution, we build the component graph: a bipartite structure where component instances are one node type and canonical conductors are the other. Each component instance has edges to each conductor it connects to, labeled with the pin numbers and pin types involved.
Pin type information, where present in the export, is attached to these edges. KiCad exports include pin type annotations (input, output, bidirectional, power input, power output, open collector, passive) from the symbol library. Altium exports carry similar data. SPICE netlists generally do not include explicit pin type data, so rules that depend on pin types produce fewer results for SPICE inputs unless the user has enriched the component definitions with a custom properties file.
The component graph is the primary structure that rule predicates query. A rule asking "does net X have at least one driver pin?" translates directly to a graph query: find all edges connected to the conductor for net X, filter for edges where pin_type is in the driver set (output, bidirectional, tristate, power output), count them. If count is zero, the rule fires.
Why data-flow over tree-walk
Our first internal prototype evaluated rules using a recursive tree walk over the parsed schematic structure. Each rule was a visitor function that traversed the relevant subtree. This worked correctly for small schematics but had two problems at scale.
First, tree-walk evaluation made it difficult to express rules that involve relationships between elements in different parts of the schematic. A rule that checks whether every ADC instance has a decoupling cap on a specific pin requires looking at the component (the ADC), its power pins, the nets those pins connect to, and then searching those nets for connected capacitors. In a tree-walk model, this involves maintaining state across multiple visit callbacks and coordinating lookups in reverse-direction structures that are expensive to build per-rule.
Second, many rules share intermediate computations. The net-to-component adjacency index, the set of all passive components by value range, the map of supply net names to their canonical conductor IDs: these are computed repeatedly under tree-walk if each rule triggers its own traversal. For a large schematic with 500+ components and 870+ active rules, this becomes prohibitive.
The data-flow model we use now materializes the component graph and a set of indexed views over it once, then evaluates all rules against the in-memory structure in a single pass. Rule predicates are compiled to query operations over the indexed views. Common subexpressions across multiple rules are identified at compile time and shared. This produces a roughly linear relationship between schematic size and evaluation time, rather than the quadratic behavior we observed with tree-walk on larger designs.
Rule evaluation and violation reporting
Each rule is a predicate with a selector and a condition. The selector identifies the objects to examine (nets matching a name pattern, components of a given designator prefix, pins of a given type on a given component class). The condition is evaluated for each selected object, and objects where the condition fails are reported as violations.
Violation records carry the canonical conductor or component identity, the rule ID and severity, the specific condition that failed, and enough context information to identify the location in the original schematic. For KiCad netlists, CADY maps violations back to sheet-relative net names and component reference designators. For Altium, it uses the component reference designator and pin number from the IPC-2581 structure.
CADY reports violations. It does not modify the netlist or suggest specific corrections. This is a deliberate boundary: the rule engine can identify that a net has no driver, but determining the correct fix requires understanding the circuit intent, which is the engineer's job. We flag what the rules catch; the engineer decides whether each violation is a real problem, an intentional exception, or a rule that needs to be updated.
Large schematic performance
The largest schematics we have processed in internal testing have been around 1,800 component instances across 12 hierarchical sheets. At that scale, the full rule evaluation run (870+ rules, full library) completes in approximately 40 to 90 seconds depending on the rule mix and the number of custom rules configured. The evaluation time is dominated by the indexed view construction in the first pass; actual predicate evaluation over the materialized graph is fast.
We are continuing to profile the net resolution step for deeply hierarchical schematics where the union-find merge count grows significantly. If you are working with unusually large designs and encounter performance issues, we want to hear about them. The current implementation has headroom for further optimization, and real-world cases are the most useful input for prioritizing where to focus.