Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 23 additions & 9 deletions src/cli/validate_extractors.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,18 @@ def empty_details() -> Details:
_Extractor = Callable[[str], Details]


def _head_line(message: str) -> str:
"""Return the first line of an exception message.

`WirebenchError.__str__` appends optional `Why:` and `Wired at:`
lines after the base message; the per-class patterns below match
against the base message only. Bullet-style payloads (used by
`BreadboardIncompatibleError`) handle their multi-line shape via
`splitlines()` directly.
"""
return message.splitlines()[0] if message else ''


def _strip_quotes(token: str) -> str:
token = token.strip()
if len(token) >= 2 and token[0] == token[-1] and token[0] in ("'", '"'):
Expand Down Expand Up @@ -109,11 +121,12 @@ def _split_part_pin_list(payload: str) -> tuple[list[str], list[str]]:

def extract_short_circuit(message: str) -> Details:
out = empty_details()
m = _SHORT_WIRE.match(message)
head = _head_line(message)
m = _SHORT_WIRE.match(head)
if m:
out['pins'] = _split_quoted_list(m.group(1))
return out
m = _SHORT_NET.match(message)
m = _SHORT_NET.match(head)
if m:
parts, pins = _split_part_pin_list(m.group(1))
out['parts'] = parts
Expand All @@ -123,7 +136,7 @@ def extract_short_circuit(message: str) -> Details:

def extract_floating_net(message: str) -> Details:
out = empty_details()
m = _FLOATING_NET.match(message)
m = _FLOATING_NET.match(_head_line(message))
if m:
parts, pins = _split_part_pin_list(m.group(1))
out['parts'] = parts
Expand All @@ -133,26 +146,27 @@ def extract_floating_net(message: str) -> Details:

def extract_incompatible_mate(message: str) -> Details:
out = empty_details()
m = _INCOMPATIBLE_MATE.match(message)
m = _INCOMPATIBLE_MATE.match(_head_line(message))
if m:
out['parts'] = [m.group(1), m.group(3)]
return out


def extract_part_configuration(message: str) -> Details:
out = empty_details()
m = _PARTCONFIG_OUT_NO_CELL.match(message)
head = _head_line(message)
m = _PARTCONFIG_OUT_NO_CELL.match(head)
if m:
out['parts'] = [m.group(1)]
out['pin'] = m.group(2)
out['pin_number'] = int(m.group(3))
return out
m = _PARTCONFIG_DRIVE_WRONG_DIR.match(message)
m = _PARTCONFIG_DRIVE_WRONG_DIR.match(head)
if m:
out['parts'] = [m.group(1)]
out['pin'] = m.group(2)
return out
m = _PARTCONFIG_TYPO_ENTRY.match(message)
m = _PARTCONFIG_TYPO_ENTRY.match(head)
if m:
out['parts'] = [m.group(1)]
out['pin'] = m.group(2)
Expand All @@ -161,7 +175,7 @@ def extract_part_configuration(message: str) -> Details:

def extract_part_parameter(message: str) -> Details:
out = empty_details()
m = _PARTPARAM_PIN_FUNCTION.match(message)
m = _PARTPARAM_PIN_FUNCTION.match(_head_line(message))
if m:
out['pin_number'] = int(m.group(1))
out['pin'] = m.group(2)
Expand All @@ -170,7 +184,7 @@ def extract_part_parameter(message: str) -> Details:

def extract_domain_crossing(message: str) -> Details:
out = empty_details()
m = _DOMAIN_CROSSING.match(message)
m = _DOMAIN_CROSSING.match(_head_line(message))
if not m:
return out
pins: list[str] = []
Expand Down
35 changes: 32 additions & 3 deletions src/framework/circuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,24 @@ def _empty_circuit_message(self) -> str:
f"`parts=[]` explicitly."
)

@staticmethod
def _collect_net_source_locations(
net_ports: Sequence[tuple[Part, Port]],
) -> list[tuple[str, int]]:
"""Deduped source locations of every `wire()` call that touched
any port on this net, preserving first-appearance order."""
collected: list[tuple[str, int]] = []
seen: set[tuple[str, int]] = set()
for _, port in net_ports:
if port.node is None:
continue
for loc in port.node.source_locations:
if loc in seen:
continue
seen.add(loc)
collected.append(loc)
return collected

def _validate_no_orphan_ports(self, parts: list[Part]) -> None:
"""Rule 2: every port wired to a port we know about must itself
belong to a part we know about.
Expand Down Expand Up @@ -129,7 +147,8 @@ def _validate_no_orphan_ports(self, parts: list[Part]) -> None:
f"the orphan as `self.<name> = …` so the "
f"framework auto-collects it, or pass "
f"`parts=[…]` explicitly listing every "
f"part."
f"part.",
source_locations=port.node.source_locations,
)

def _auto_collect_parts(self) -> list[Part]:
Expand Down Expand Up @@ -202,14 +221,19 @@ def _validate(self, parts: list[Part]) -> None:
# boundaries.
from framework.export.nets import compute_logical_nets
shorts: list[str] = []
short_locations: list[tuple[str, int]] = []
floats: list[str] = []
float_locations: list[tuple[str, int]] = []
for net in compute_logical_nets(self):
outs = [(o, p) for (o, p) in net.ports if p.direction is Direction.OUT]
bidirs = [(o, p) for (o, p) in net.ports if p.direction is Direction.BIDIR]

if len(outs) > 1:
shorts.append(', '.join(
f"'{type(o).__name__}.{p.name}'" for o, p in outs))
for loc in self._collect_net_source_locations(net.ports):
if loc not in short_locations:
short_locations.append(loc)
elif (len(outs) == 0 and len(bidirs) > 1
and not net.dynamically_driven):
# `dynamically_driven` is the designer's explicit
Expand All @@ -219,16 +243,21 @@ def _validate(self, parts: list[Part]) -> None:
# detection above stays strict regardless.
floats.append(', '.join(
f"'{type(o).__name__}.{p.name}'" for o, p in bidirs))
for loc in self._collect_net_source_locations(net.ports):
if loc not in float_locations:
float_locations.append(loc)

if shorts:
raise ShortCircuitError(
"Short circuit on logical net — multiple drivers: "
+ '; '.join(shorts)
+ '; '.join(shorts),
source_locations=short_locations,
)
if floats:
raise FloatingNetError(
"Floating logical net — multiple passive BIDIRs with no driver: "
+ '; '.join(floats)
+ '; '.join(floats),
source_locations=float_locations,
)

# Duplicate-refdes detection. Walks only refdes-bearing children
Expand Down
Loading