Analyzer

Rig model plus board in, solved rig out — or a diagnostic naming the pin. Everything this stage rejects is something the hardware gets wrong: a plug that does not mate, a position that is not routed, two devices at one address, no chip-select left to allocate.

Each pass is a value function returning its own piece plus its diagnostics; rigc.analyzer.analyze is the one composer that runs them in order.

rigc.analyzer

The analyzer: rig model + board -> solved rig.

Each pass is a value function taking exactly the prior pieces it needs and returning (its piece, diagnostics); analyze below is the ONE composing function that assembles the solved model, always in the same fixed order – sockets -> gpio nets -> addresses -> CS -> wires -> net conflicts -> labels.

Skip-don’t-abort is the observable contract this composer preserves structurally rather than by convention: a slot whose socket never resolved (analyzer/sockets.py’s resolve_sockets) is simply absent from resolution.sockets, and every later pass already guards its own lookup through the accessor family (analyzer/socketmap.py’s for_ref/ for_bus_device) – there is no separate “abort” path to avoid taking.

Solved is the emitter’s input contract: a frozen value the emitter slice consumes unchanged. Freezing it puts ownership in the type rather than in prose alone – a pass rebinding a field on a model another pass already produced is this codebase’s recurring failure mode, and here it is a TypeError instead. wires holds the ROUTE-RESOLVED wire list analyzer/wires.py returns as a field of its own, rather than a route resolved by mutating wire.route in place on the rig’s own Wire objects, which this codebase’s passes never do to a value they were handed.

rigc.analyzer.sockets

Mating and socket resolution, with carrier/mux composition, value-shaped throughout: resolve_sockets returns the instance->slot-> BoardSocket map plus every mux-channel scope entry composition creates, alongside its diagnostics – no mutable accumulator, no diags side channel.

Two pieces are pulled out as PURE value functions on their own:

mating_ok / subset_gaps – plug-type-vs-socket-type and needed-vs-offered

bus decisions, each a one-line predicate over plain strings/sets.

compose_socket – (parent socket, exposure) -> synthesized

socket + scope entries, over PLAIN ExposedSocket/BoardSocket values – no Instance/Rig/Shield needed to call it.

resolve_sockets is the pass: it walks rig.instances, recursing through carrier chains (stack-guarded against cycles, memoizing into the returned map as it goes), and folds in the stackability check once every instance’s socket is known. Skip-don’t-abort is structural here: an instance whose socket never resolves is simply absent from the returned map, and every later pass already skips a missing entry rather than aborting.

rigc.analyzer.socketmap

The ONE seam every pass and emitter module resolves “the socket a reference/device targets” through: a resolution is keyed per SLOT rather than per instance, so SocketResolution.sockets/Solved.sockets is Sockets below (instance name -> slot name -> socket), never a bare Dict[str, BoardSocket].

Three functions, matching the two granularities a claim can name a socket at – PER-REFERENCE (a gpio/pwm/adc claim names its own plug by phandle) and PER-BUS-GROUP (a device’s bus binds to exactly one plug) – plus the slot-enumeration helper the per-slot renderers (sheet.py) need. This is the buskind.py precedent one level up: one shared implementation so a caller gating behavior on “which slot does this belong to” cannot drift into three copies of the same lookup, instead of a bare per-instance dict lookup of this map’s own two levels appearing anywhere else in analyzer or emitter code.

Every function here is read-only over its arguments and returns a reference into the resolution map it was handed, never a copy – the returned BoardSocket is owned by whichever pass built the map (analyzer/sockets.py’s resolve_sockets), never by the caller.

rigc.analyzer.gpio

Net identity, GPIO/PWM/ADC claims, jumper resolution, and the final net conflict report, value-shaped throughout.

Net IDENTITY is sharing: soc_net resolves a socket position through the board’s own gpio-map down to the actual SoC pin, so two DIFFERENT sockets whose positions map to the same pin are the SAME net – a pure function of (socket, position), directly unit-testable. role_of is the other already-value-shaped contract this module keeps.

collect_gpio_nets is the pass: it walks every resolved instance’s device gpio/pwm/adc refs, building the net-claim map plus jumper-resolved positions and pwm/adc channel resolutions, entirely as a RETURNED value (GpioNets) – check_nets is a SEPARATE, later function: net collection happens before CS allocation, but net CONFLICT checking happens after, since CS allocation contributes further claims into the same net-claim map – see analyzer/cs.py and analyzer/__init__.py’s composer, which merges the two claim sets before calling check_nets.

rigc.analyzer.addresses

Address allocation. Per I2C-bus SCOPE (a mux channel is its own NEW scope), a fixed (copper reg) member wins outright, a pinned (config: strap) member resolves through the strap’s own domain, and everything else allocates freely from that same domain – each group sorted through the one stable allocation order (analyzer/ordering.py’s allocation_key), never rig-file declaration order.

The value-shaped core: allocate_scope_addresses is the pure contract this module exists to make unit-testable on its own – given one scope’s members in allocation order (some copper-fixed, some rig-pinned, some free), each already carrying its OWN address domain where one applies, assign each an address (+ strap state), or report a same-address conflict or a free member’s domain exhaustion. No Rig/Instance/Shield/BoardSocket needed to call it, mirroring analyzer/cs.py’s allocate_cs_positions/CsMember exactly. _allocate_scope is the WIRING: it builds one scope’s AddressMember list from the rig model (in the three groups’ allocation order), calls the core, and translates its placements/problems back into this pass’s own AddressAllocation fields and diagnostics – the only place strap names, device labels, and bus labels ever enter the picture.

A rig-pinned member’s domain membership (phys-pin) is checked INSIDE the core, in the same single ordered pass as everything else, rather than by the wrapper up front – it must interleave with same-address conflicts and free-domain exhaustion in exactly the members’ own discovery order, which only a single shared pass can guarantee.

rigc.analyzer.cs

CS pool allocation: where and how the final cs-gpios property is calculated, split into a value-shaped contract:

effective_cs_pool – the pool-MERGE fallback: a socket’s own

authored override wins, else the connector type’s binding default.

allocate_cs_positions – THE algorithm, and the part this module exists

to make unit-testable on its own: given an ORDERED pool (as (position, net-identity) pairs – net identity, not a bare position index, because two DIFFERENT sockets in one SPI scope are compared through the SAME SoC pin), the net identities ALREADY taken, and the scope’s members in allocation order (some copper-fixed), assign each a position or report the pool exhausted. No Rig/Instance/Shield/Board needed to call it.

allocate_cs – the PASS: walks rig.instances, groups SPI-bus

members into scopes (a mux channel is its own new scope), builds each member’s CsMember from its resolved socket + connector type, and folds the placements into cs/cs_gpios plus the NEW net claims (for the composer to merge into the shared net-claim map before the final conflict check, analyzer/gpio.py’s check_nets).

rigc.analyzer.wires

Wires and emission feasibility of routes. No frozen golden covers this family (phys-wire) – every diagnostic here is checked by hand differential rather than a golden byte comparison.

Value-shaped and non-mutating: a route: via <name> string resolves to its connector-type position INDEX without mutating wire.route in place – this module returns a NEW list of Wire values with the route already resolved, so the pass composes like every other one here ((its piece, diagnostics)), never writing into a Rig it was handed.

rigc.analyzer.ordering

Allocation ordering. Every allocator (addresses, CS) sorts its scope members through this ONE function before assigning anything, so a rig author reordering instances: never changes what gets allocated where – deterministic, order-independent, pinnable.

(socket, instance name, device name), read straight off the Instance/Device values already in hand plus the resolved socket the caller already has – no Rig needed, which is what makes it a value function on its own.

rigc.analyzer.labels

Emission feasibility of generated labels. Strong contract: the emitter never fails, so the deterministic label scheme <instance>_<shield label> must be collision-free HERE. Runs over every declared instance regardless of whether its socket resolved: a label collision is a property of two instance/device NAME pairs alone, needing no board/socket information at all, so it is not one of the passes skip-don’t-abort applies to.