Part 1 · Why do hooks exist at all?
The one job: "A pattern's template has a fixed number of devices. But some real structures have a variable number. How do we match those?"
A same_net template is rigid: it says "exactly two nmos, sources tied." Perfect for a differential pair.
But a bias generator is not fixed-size. It has one master reference and then however many output "legs" the circuit happens to need — anywhere from 1 to 7. You can't write a template for "1 to 7 of something."
So the recognizer offers an escape hatch. A pattern can name a hook — a "module:function" path. After the base template matches, recognize() calls the hook, handing it the match, and lets the hook discover the rest by walking the netlist directly.
The analogy: a building inspector's clipboard
The template finds the front door (the master reference). The hook is the inspector who then walks the whole house counting rooms, and either signs off with the full room list attached — or condemns the building (returns None).
Part 2 · The hook contract
Every hook has the identical signature and one of two possible outcomes:
def hook(assignment, pins, netlist) -> HookMatch | None
A returned HookMatch carries two lists that get merged into the final structure:
extra_devices— appended toRecognizedStructure.devices.extra_pins— merged intoRecognizedStructure.pins(e.g.leg1_out,leg2_out,vdd).
Recall from the recognizer exactly where this fires:
if pattern.hook:
result = _resolve_hook(pattern.hook)(assignment, pins, netlist)
if result is None:
continue # hook rejected → skip this match
devices_list = devices_list + result.extra_devices
pins = {**pins, **result.extra_pins}
Part 3 · The "legs" idea — the motivating case
The one job: "Starting from one diode-connected master reference, find every output leg that mirrors it."
A current-mirror bias generator looks like a comb: one master reference tooth, then many parallel legs, each mirroring the reference current out to a different rail.
A "leg" is a self-contained little group: an nmos whose gate ties to the master's ibias node and source to gnd (so it mirrors the reference current), paired with whatever delivers its output — a diode-connected pmos, or a resistor, depending on the flavor.
Part 4 · One leg, traced by hand
Take diode_connected_mosfet_bias_legs. The master reference is mref with:
mref.terminals = {d:"ibias", g:"ibias", s:"gnd!", b:"gnd!"}
ibias_net = "ibias" gnd_net = "gnd!"
The hook scans every device looking for an nmos leg. Consider candidate mL:
| Check | mL | Pass? |
|---|---|---|
| type == nmos | nmos | ✓ |
| gate == ibias_net | g = "ibias" | ✓ |
| source == gnd_net | s = "gnd!" | ✓ |
It mirrors the master! Its drain net (say "n_leg1") becomes the leg's output. Now find the pmos sitting on that drain, diode-connected:
pmos_leg: p.type=="pmos" and p.d==out_net and p.g==out_net and p.s==p.b
Found pL with d=g="n_leg1", s=b="vdd!". The leg is complete:
leg_count = 1
extra_devices += [mL, pL]
extra_pins["leg1_out"] = "n_leg1"
extra_pins.setdefault("vdd", "vdd!") # first leg records the supply
claimed.add(mL.ref); claimed.add(pL.ref)
The claimed set is the quiet hero here.
It stops a device being counted into two legs. Every candidate loop begins with if …ref in claimed: continue, so the scan naturally partitions the netlist into disjoint legs.
Part 5 · The three leg-hunters — same skeleton, different legs
Three hooks share the exact same shape (find mirrors of a reference, pair each with its output device) and differ only in what a "leg" is made of:
| Hook | Reference (mref) | A leg is… | Records |
|---|---|---|---|
| diode_connected_mosfet_bias_legs | diode nmos on gnd | (nmos mirror, diode pmos) | vdd |
| magic_battery_bias_legs | diode pmos on vdd | (pmos mirror, diode nmos) | gnd |
| resistor_bias_legs | diode pmos on vdd | (pmos mirror, resistor) | gnd |
They're mirror images with the polarity flipped — the "why" behind writing three near-identical functions instead of one generic one is readability: each reads top-to-bottom as the physical story of that specific bias style.
The fourth, richer one: constructed_bias_legs
This is the heavyweight. It handles a constructed multi-reference generator with a two-pass sweep:
The drain condition in Pass 2 ("its drain must host a diode or a resistor to gnd") is what keeps consumer devices out — a second-stage pmos gated by a bias rail has neither on its drain, so it's correctly ignored.
Part 6 · Guard hooks — rejection as the whole point
Not every hook extends a match. Some exist purely to reject false matches. These are the simplest hooks in the file:
def resistor_tail_vdd_check(assignment, pins, netlist):
if assignment["r1"].terminals["t1"] != "vdd!":
return None # not a tail resistor → reject
return HookMatch(extra_devices=[], extra_pins={})
Why needed? The resistor_tail_vdd pattern's template is just "one resistor" — with no constraints it would match every resistor in the netlist (load resistors, degeneration resistors, bias resistors). The guard says "only if one end is the global vdd! rail," pinning it to the actual tail resistor.
Four such guards exist (tail_vdd, tail_gnd, load_vdd, load_gnd) — each a one-line rail check that a template alone can't express.
The tricky rejection contract between hooks
Notice the interplay: constructed_bias_legs returns None when it finds only the plain NMOS-referenced shape, deliberately ceding that match to diode_connected_mosfet_bias_legs (listed after it). This keeps legacy single-flavor netlists recognized under their historical names. The rejection isn't a failure — it's a handoff.
Part 7 · The hidden idea
Hooks turn the recognizer from a fixed-template matcher into something strictly more powerful:
The template supplies the anchor — the one device the search can hang onto. The hook supplies the unbounded exploration — following nets outward from that anchor to discover however many devices are actually there. It's the same trick a regex engine uses when it hands off to a callback for a match that plain regex syntax can't express.
And the design keeps hooks pure and total in the same spirit as the rest of Layer 1: a hook only ever reads the netlist and returns a value. It never mutates anything, never picks between competing structures — it just says "yes, and here's the rest" or "no." All the disambiguation still waits for Layer 2.
In one sentence: hooks.py is a set of building inspectors that start from one anchor device a template found, walk the netlist to discover a variable number of extra devices (a bias generator's legs), and either sign off with the full list attached or reject the match outright.
Looking ahead → Part 5 · functional_block_recognizer.py is Layer 2 — where all those overlapping SR candidates finally get resolved into named functional slots.