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
16 changes: 15 additions & 1 deletion src/cli/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,20 @@ def _emit(payload: dict[str, Any]) -> None:
sys.stdout.write('\n')


def _details_with_remediation(e: WirebenchError) -> dict[str, Any]:
"""Build the `details` dict for a framework exception, merging the
structured fields the regex extractor scrapes from the message with
the high-confidence remediation hint the exception class produces
(when one applies). Omits the `remediation` key entirely when the
class returned `None` — keeps the JSON shape minimal for the
common low-confidence case."""
details: dict[str, Any] = dict(extract(type(e).__name__, str(e)))
remediation = e.suggested_remediation()
if remediation is not None:
details['remediation'] = remediation
return details


def _error(message: str, error_class: str, *, design: str | None = None) -> int:
_emit({
'status': 'error',
Expand Down Expand Up @@ -216,7 +230,7 @@ def run_validate(argv: list[str]) -> int:
'design': target.__name__,
'error_class': type(e).__name__,
'message': str(e),
'details': extract(type(e).__name__, str(e)),
'details': _details_with_remediation(e),
})
return 1
except (ValueError, TypeError) as e:
Expand Down
6 changes: 5 additions & 1 deletion src/components/chips/concepts/nor_latch.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,11 @@ def evaluate(self) -> None:
s = bool(Digital(self._ports['s'].value))
r = bool(Digital(self._ports['r'].value))
if s and r:
raise ForbiddenStateError("Invalid: S and R both active")
raise ForbiddenStateError(
"Invalid: S and R both active",
state_signature='sr_latch_both_active',
port_names=('s', 'r'),
)
if s:
self._q = True
elif r:
Expand Down
36 changes: 34 additions & 2 deletions src/framework/circuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,10 @@ def _validate(self, parts: list[Part]) -> None:
]
if unconnected:
raise UnconnectedPinError(
f"Unconnected mandatory port(s): {', '.join(unconnected)}"
f"Unconnected mandatory port(s): {', '.join(unconnected)}",
port_refs=tuple(
ref.strip("'") for ref in unconnected
),
)

# Net-aware short-circuit / floating detection. Delegated to
Expand All @@ -222,15 +225,19 @@ def _validate(self, parts: list[Part]) -> None:
from framework.export.nets import compute_logical_nets
shorts: list[str] = []
short_locations: list[tuple[str, int]] = []
short_drivers: list[str] = []
floats: list[str] = []
float_locations: list[tuple[str, int]] = []
float_port_refs: list[str] = []
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 o, p in outs:
short_drivers.append(f"{type(o).__name__}.{p.name}")
for loc in self._collect_net_source_locations(net.ports):
if loc not in short_locations:
short_locations.append(loc)
Expand All @@ -243,20 +250,32 @@ 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 o, p in bidirs:
float_port_refs.append(f"{type(o).__name__}.{p.name}")
for loc in self._collect_net_source_locations(net.ports):
if loc not in float_locations:
float_locations.append(loc)

if shorts:
# Carry the structured driver list *only when one net is
# shorted* — multi-net diagnostics would conflate drivers
# from independent shorts, and the remediation only fires
# at the two-driver canonical shape anyway.
drivers: tuple[str, ...] = (
tuple(short_drivers) if len(shorts) == 1 else ()
)
raise ShortCircuitError(
"Short circuit on logical net — multiple drivers: "
+ '; '.join(shorts),
drivers=drivers,
source_locations=short_locations,
)
if floats:
raise FloatingNetError(
"Floating logical net — multiple passive BIDIRs with no driver: "
+ '; '.join(floats),
kind='multi_bidir',
port_refs=tuple(float_port_refs),
source_locations=float_locations,
)

Expand All @@ -275,8 +294,21 @@ def _validate(self, parts: list[Part]) -> None:
else:
seen[key] = label
if collisions:
# Single canonical duplicate → carry the refdes string so
# the remediation can name it. Multi-collision diagnostics
# are common enough that a generic suggestion would still
# apply, but the framework names only the specific case it
# can be confident about.
duplicate_refdes = ''
if len(collisions) == 1:
# `collisions[0]` is "'X.Rn' and 'Y.Rn'" — extract the
# shared refdes from the right-hand label.
tail = collisions[0].rsplit('.', 1)[-1]
duplicate_refdes = tail.rstrip("'")
raise RefdesError(
f"Duplicate refdes: {'; '.join(collisions)}"
f"Duplicate refdes: {'; '.join(collisions)}",
kind='duplicate',
duplicate_refdes=duplicate_refdes,
)

@property
Expand Down
Loading