digsim-logic-simulator 0.12.0__py3-none-any.whl → 0.13.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of digsim-logic-simulator might be problematic. Click here for more details.

Files changed (40) hide show
  1. digsim/app/__main__.py +16 -12
  2. digsim/app/gui/_circuit_area.py +32 -21
  3. digsim/app/gui/_component_selection.py +2 -0
  4. digsim/app/gui_objects/__init__.py +1 -1
  5. digsim/app/gui_objects/_buzzer_object.py +6 -6
  6. digsim/app/gui_objects/_component_context_menu.py +1 -1
  7. digsim/app/gui_objects/_component_object.py +10 -9
  8. digsim/app/gui_objects/_component_port_item.py +0 -2
  9. digsim/app/gui_objects/_dip_switch_object.py +5 -4
  10. digsim/app/gui_objects/_gui_note_object.py +10 -11
  11. digsim/app/gui_objects/_gui_object_factory.py +1 -1
  12. digsim/app/gui_objects/_image_objects.py +7 -5
  13. digsim/app/gui_objects/_label_object.py +3 -2
  14. digsim/app/gui_objects/_logic_analyzer_object.py +18 -9
  15. digsim/app/gui_objects/_seven_segment_object.py +10 -3
  16. digsim/app/gui_objects/_shortcut_objects.py +3 -2
  17. digsim/app/model/_model.py +7 -3
  18. digsim/app/model/_model_components.py +1 -4
  19. digsim/app/model/_model_new_wire.py +2 -1
  20. digsim/app/model/_model_objects.py +1 -5
  21. digsim/app/model/_model_settings.py +1 -1
  22. digsim/app/model/_model_shortcuts.py +0 -14
  23. digsim/app/settings/_component_settings.py +1 -1
  24. digsim/app/settings/_shortcut_dialog.py +3 -4
  25. digsim/circuit/_circuit.py +52 -38
  26. digsim/circuit/components/_button.py +1 -5
  27. digsim/circuit/components/_yosys_component.py +1 -1
  28. digsim/circuit/components/atoms/__init__.py +1 -0
  29. digsim/circuit/components/atoms/_component.py +14 -4
  30. digsim/circuit/components/atoms/_port.py +45 -29
  31. digsim/storage_model/_app.py +7 -2
  32. digsim/storage_model/_circuit.py +15 -7
  33. digsim/synth/__main__.py +2 -2
  34. digsim/synth/_synthesis.py +4 -1
  35. digsim/utils/_yosys_netlist.py +9 -3
  36. {digsim_logic_simulator-0.12.0.dist-info → digsim_logic_simulator-0.13.0.dist-info}/METADATA +1 -1
  37. {digsim_logic_simulator-0.12.0.dist-info → digsim_logic_simulator-0.13.0.dist-info}/RECORD +40 -40
  38. {digsim_logic_simulator-0.12.0.dist-info → digsim_logic_simulator-0.13.0.dist-info}/WHEEL +0 -0
  39. {digsim_logic_simulator-0.12.0.dist-info → digsim_logic_simulator-0.13.0.dist-info}/licenses/LICENSE.md +0 -0
  40. {digsim_logic_simulator-0.12.0.dist-info → digsim_logic_simulator-0.13.0.dist-info}/top_level.txt +0 -0
@@ -271,7 +271,7 @@ class ComponentSettingsCheckBoxWidthBool(ComponentSettingsBase):
271
271
  checkbox = QCheckBox(f"bit{checkbox_id}")
272
272
  checkbox.setChecked(True)
273
273
  checkbox.stateChanged.connect(self._update)
274
- self.layout().addWidget(checkbox, checkbox_id % 8, checkbox_id / 8, 1, 1)
274
+ self.layout().addWidget(checkbox, checkbox_id % 8, checkbox_id // 8, 1, 1)
275
275
  self._bit_checkboxes.append(checkbox)
276
276
  disable_all = QPushButton("Disable All")
277
277
  disable_all.clicked.connect(self._disable_all)
@@ -20,12 +20,11 @@ class ShortcutDialog(QDialog):
20
20
  self.layout().addWidget(QLabel("Select shortcut key for component"))
21
21
  self._key_selector = QComboBox(parent)
22
22
  for key in "1234567890":
23
- shortcut_key = str(key)
24
- shortcut_string = shortcut_key
25
- shortcut_component = app_model.shortcuts.get_component(shortcut_key)
23
+ shortcut_string = key
24
+ shortcut_component = app_model.shortcuts.get_component(key)
26
25
  if shortcut_component is not None:
27
26
  shortcut_string += f" - {shortcut_component.name()}"
28
- self._key_selector.addItem(shortcut_string, userData=shortcut_key)
27
+ self._key_selector.addItem(shortcut_string, userData=key)
29
28
  self.layout().addWidget(self._key_selector)
30
29
  self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
31
30
  self.buttonBox.accepted.connect(self.accept)
@@ -7,13 +7,14 @@ Module that handles the circuit simulation of components
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
- import os
10
+ import heapq
11
+ import pathlib
11
12
  from typing import Tuple
12
13
 
13
14
  from digsim.storage_model import CircuitDataClass, CircuitFileDataClass
14
15
 
15
16
  from ._waves_writer import WavesWriter
16
- from .components.atoms import Component, DigsimException, PortOutDelta
17
+ from .components.atoms import VALUE_TYPE, Component, DigsimException, PortOutDelta
17
18
 
18
19
 
19
20
  class CircuitError(DigsimException):
@@ -26,10 +27,10 @@ class CircuitEvent:
26
27
  delta events in the simulation.
27
28
  """
28
29
 
29
- def __init__(self, time_ns: int, port: PortOutDelta, value: int | str | None):
30
+ def __init__(self, time_ns: int, port: PortOutDelta, value: VALUE_TYPE):
30
31
  self._time_ns: int = time_ns
31
32
  self._port: PortOutDelta = port
32
- self._value: int | str | None = value
33
+ self._value: VALUE_TYPE = value
33
34
 
34
35
  @property
35
36
  def time_ns(self) -> int:
@@ -42,7 +43,7 @@ class CircuitEvent:
42
43
  return self._port
43
44
 
44
45
  @property
45
- def value(self) -> int | str | None:
46
+ def value(self) -> VALUE_TYPE:
46
47
  """Get the delta cycle value of this event"""
47
48
  return self._value
48
49
 
@@ -50,7 +51,7 @@ class CircuitEvent:
50
51
  """Return True if the in the event is the same as"""
51
52
  return port == self._port
52
53
 
53
- def update(self, time_ns: int, value: int | str | None):
54
+ def update(self, time_ns: int, value: VALUE_TYPE):
54
55
  """Update the event with a new time (ns) and a new value"""
55
56
  self._time_ns = time_ns
56
57
  self._value = value
@@ -58,6 +59,14 @@ class CircuitEvent:
58
59
  def __lt__(self, other) -> bool:
59
60
  return other.time_ns > self.time_ns
60
61
 
62
+ def __eq__(self, other) -> bool:
63
+ return (
64
+ isinstance(other, CircuitEvent)
65
+ and self._port == other._port
66
+ and self._time_ns == other._time_ns
67
+ and self._value == other._value
68
+ )
69
+
61
70
 
62
71
  class Circuit:
63
72
  """Class thay handles the circuit simulation"""
@@ -65,6 +74,7 @@ class Circuit:
65
74
  def __init__(self, name: str | None = None, vcd: str | None = None):
66
75
  self._components: dict[str, Component] = {}
67
76
  self._circuit_events: list[CircuitEvent] = []
77
+ self._events_by_port: dict[PortOutDelta, CircuitEvent] = {}
68
78
  self._name: str | None = name
69
79
  self._time_ns: int = 0
70
80
  self._folder: str | None = None
@@ -86,10 +96,7 @@ class Circuit:
86
96
  @property
87
97
  def components(self) -> list[Component]:
88
98
  """Get the components in this circuit"""
89
- comp_array = []
90
- for _, comp in self._components.items():
91
- comp_array.append(comp)
92
- return comp_array
99
+ return list(self._components.values())
93
100
 
94
101
  def load_path(self, path) -> str:
95
102
  """Get the load path relative to the circuit path"""
@@ -100,7 +107,9 @@ class Circuit:
100
107
  def store_path(self, path) -> str:
101
108
  """Get the store path relative to the circuit path"""
102
109
  if self._folder is not None:
103
- return os.path.relpath(path, self._folder)
110
+ return str(
111
+ pathlib.Path(path).resolve().absolute().relative_to(pathlib.Path(self._folder))
112
+ )
104
113
  return path
105
114
 
106
115
  def delete_component(self, component: Component):
@@ -110,16 +119,13 @@ class Circuit:
110
119
 
111
120
  def get_toplevel_components(self) -> list[Component]:
112
121
  """Get toplevel components in the circuit"""
113
- toplevel_components = []
114
- for _, comp in self._components.items():
115
- if comp.is_toplevel():
116
- toplevel_components.append(comp)
117
- return toplevel_components
122
+ return [comp for comp in self._components.values() if comp.is_toplevel()]
118
123
 
119
124
  def init(self):
120
125
  """Initialize circuit and components (and ports)"""
121
126
  self._time_ns = 0
122
127
  self._circuit_events = []
128
+ self._events_by_port = {}
123
129
  if self._vcd is not None:
124
130
  self._vcd_init()
125
131
  for _, comp in self._components.items():
@@ -175,22 +181,32 @@ class Circuit:
175
181
  Process one simulation event
176
182
  Return False if ther are now events of if the stop_time has passed
177
183
  """
178
- if len(self._circuit_events) == 0:
179
- return False, False
180
- self._circuit_events.sort()
181
- if stop_time_ns is None or self._circuit_events[0].time_ns > stop_time_ns:
182
- return False, False
183
- event = self._circuit_events.pop(0)
184
- # print(
185
- # f"Execute event {event.port.path()}.{event.port.name()}"
186
- # " {event.time_ns} {event.value}"
187
- # )
188
- self._time_ns = event.time_ns
189
- event.port.delta_cycle(event.value)
190
- toplevel = event.port.parent().is_toplevel()
191
- if self._vcd is not None:
192
- self._vcd.write(event.port, self._time_ns)
193
- return True, toplevel
184
+ while self._circuit_events:
185
+ event = heapq.heappop(self._circuit_events)
186
+
187
+ # Check if this is the latest event for this port
188
+ if event != self._events_by_port.get(event.port):
189
+ # This is a stale event, ignore it
190
+ continue
191
+
192
+ if stop_time_ns is not None and event.time_ns > stop_time_ns:
193
+ # Put the event back if it's after the stop time
194
+ heapq.heappush(self._circuit_events, event)
195
+ return False, False
196
+
197
+ del self._events_by_port[event.port]
198
+ # print(
199
+ # f"Execute event {event.port.path()}.{event.port.name()}"
200
+ # f" {event.time_ns} {event.value}"
201
+ # )
202
+ self._time_ns = event.time_ns
203
+ event.port.delta_cycle(event.value)
204
+ toplevel = event.port.parent().is_toplevel()
205
+ if self._vcd is not None:
206
+ self._vcd.write(event.port, self._time_ns)
207
+ return True, toplevel
208
+
209
+ return False, False
194
210
 
195
211
  def _is_toplevel_event(self) -> bool:
196
212
  if len(self._circuit_events) == 0:
@@ -231,15 +247,13 @@ class Circuit:
231
247
  if stop_time_ns >= self._time_ns:
232
248
  self.run(ns=stop_time_ns - self._time_ns)
233
249
 
234
- def add_event(self, port: PortOutDelta, value: int | str | None, propagation_delay_ns: int):
250
+ def add_event(self, port: PortOutDelta, value: VALUE_TYPE, propagation_delay_ns: int):
235
251
  """Add delta cycle event, this will also write values to .vcd file"""
236
252
  event_time_ns = self._time_ns + propagation_delay_ns
237
253
  # print(f"Add event {port.parent().name()}:{port.name()} => {value}")
238
- for event in self._circuit_events:
239
- if event.is_same_event(port):
240
- event.update(event_time_ns, value)
241
- return
242
- self._circuit_events.append(CircuitEvent(event_time_ns, port, value))
254
+ event = CircuitEvent(event_time_ns, port, value)
255
+ self._events_by_port[port] = event
256
+ heapq.heappush(self._circuit_events, event)
243
257
 
244
258
  def add_component(self, component: Component):
245
259
  """Add component to circuit"""
@@ -3,21 +3,17 @@
3
3
 
4
4
  """A PushButton component"""
5
5
 
6
- import logging
7
-
8
6
  from .atoms import CallbackComponent, PortOutImmediate
9
7
 
10
8
 
11
9
  class PushButton(CallbackComponent):
12
10
  """PushButton component class"""
13
11
 
14
- def __init__(self, circuit, name=None, inverted=False):
12
+ def __init__(self, circuit, name=None):
15
13
  super().__init__(circuit, name)
16
14
  portout = PortOutImmediate(self, "O")
17
15
  self.add_port(portout)
18
16
  portout.update_parent(True)
19
- if inverted:
20
- logging.warning("Setting 'inverted' has been removed")
21
17
 
22
18
  def default_state(self):
23
19
  self.release()
@@ -26,7 +26,7 @@ class YosysComponent(MultiComponent):
26
26
  def __init__(self, circuit, path=None, name=None, nets=True):
27
27
  super().__init__(circuit, name)
28
28
  self._circuit = circuit
29
- self._path = path
29
+ self._path = str(path)
30
30
  self._gates_comp = None
31
31
  self._net_comp = None
32
32
  self._netlist_module = None
@@ -11,6 +11,7 @@ from ._component import ( # noqa: F401
11
11
  )
12
12
  from ._digsim_exception import DigsimException # noqa: F401
13
13
  from ._port import ( # noqa: F401
14
+ VALUE_TYPE,
14
15
  Port,
15
16
  PortConnectionError,
16
17
  PortIn,
@@ -63,10 +63,20 @@ class Component(abc.ABC):
63
63
  self._ports = []
64
64
 
65
65
  def path(self) -> str:
66
- """Get component path"""
67
- if self._parent is not None:
68
- return f"{self._parent.path()}.{self.name()}"
69
- return f"{self.name()}"
66
+ """Get component path (iterative)"""
67
+ path_parts = []
68
+ current_component = self
69
+ while current_component is not None:
70
+ name = current_component.name()
71
+ if not isinstance(name, str):
72
+ raise TypeError(
73
+ f"Component name is not a string: "
74
+ f"{name} ({type(name)}), "
75
+ f"component: {current_component.__class__.__name__}"
76
+ )
77
+ path_parts.append(name)
78
+ current_component = current_component._parent
79
+ return ".".join(reversed(path_parts))
70
80
 
71
81
  @property
72
82
  def ports(self):
@@ -6,10 +6,14 @@
6
6
  from __future__ import annotations
7
7
 
8
8
  import abc
9
+ from typing import Literal, Optional, Union
9
10
 
10
11
  from ._digsim_exception import DigsimException
11
12
 
12
13
 
14
+ VALUE_TYPE = Union[int, Literal["X"]]
15
+
16
+
13
17
  class PortConnectionError(DigsimException):
14
18
  """Exception for illegal connections"""
15
19
 
@@ -23,8 +27,8 @@ class Port(abc.ABC):
23
27
  self._width: int = width # The bit-width of this port
24
28
  self._output: bool = output # Is this port an output port
25
29
  self._wired_ports: list[Port] = [] # The ports that this port drives
26
- self._value: int | str | None = None # The value of this port
27
- self._edge_detect_value: int | str | None = "X" # Last edge detect value
30
+ self._value: VALUE_TYPE = "X" # The value of this port
31
+ self._edge_detect_value: VALUE_TYPE = "X" # Last edge detect value
28
32
  self.init() # Initialize the port
29
33
 
30
34
  def init(self):
@@ -39,12 +43,12 @@ class Port(abc.ABC):
39
43
  return self._wired_ports
40
44
 
41
45
  @property
42
- def value(self) -> int | str | None:
46
+ def value(self) -> VALUE_TYPE:
43
47
  """Get the value of the port, can be "X" """
44
48
  return self._value
45
49
 
46
50
  @value.setter
47
- def value(self, value: int | str | None):
51
+ def value(self, value: VALUE_TYPE):
48
52
  """Set the value of the port"""
49
53
  self.set_value(value)
50
54
 
@@ -60,7 +64,7 @@ class Port(abc.ABC):
60
64
  driver = self.get_driver()
61
65
  if driver is not None:
62
66
  driver.disconnect(self)
63
- for port in self._wired_ports:
67
+ for port in self._wired_ports[:]:
64
68
  self.disconnect(port)
65
69
  self._width = width
66
70
 
@@ -98,7 +102,7 @@ class Port(abc.ABC):
98
102
  """Get parent component"""
99
103
  return self._parent
100
104
 
101
- def update_wires(self, value: int | str | None):
105
+ def update_wires(self, value: VALUE_TYPE):
102
106
  """Update connected wires (and self._value) with value"""
103
107
  if self._value == value:
104
108
  return
@@ -106,11 +110,21 @@ class Port(abc.ABC):
106
110
  for port in self._wired_ports:
107
111
  port.value = self._value
108
112
 
109
- def get_wired_ports_recursive(self) -> list[Port]:
110
- """Get all connected ports (recursive)"""
111
- all_wired_ports = [self]
112
- for port in self._wired_ports:
113
- all_wired_ports.extend(port.get_wired_ports_recursive())
113
+ def get_wired_ports_recursive(self, processed_ports: Optional[set] = None) -> list[Port]:
114
+ """Get all connected ports (iterative), avoiding duplicates."""
115
+ if processed_ports is None:
116
+ processed_ports = set()
117
+
118
+ all_wired_ports = []
119
+ ports_to_process = [self]
120
+
121
+ while ports_to_process:
122
+ port = ports_to_process.pop()
123
+ if port in processed_ports:
124
+ continue
125
+ processed_ports.add(port)
126
+ all_wired_ports.append(port)
127
+ ports_to_process.extend(port.wired_ports)
114
128
  return all_wired_ports
115
129
 
116
130
  def is_output(self) -> bool:
@@ -144,7 +158,7 @@ class Port(abc.ABC):
144
158
  return falling_edge
145
159
 
146
160
  @abc.abstractmethod
147
- def set_value(self, value: int | str | None):
161
+ def set_value(self, value: VALUE_TYPE):
148
162
  """Set value on port"""
149
163
 
150
164
  @abc.abstractmethod
@@ -195,7 +209,7 @@ class PortWire(Port):
195
209
  super().__init__(parent, name, width, output)
196
210
  self._port_driver: Port | None = None # The port that drives this port
197
211
 
198
- def set_value(self, value: int | str | None):
212
+ def set_value(self, value: VALUE_TYPE):
199
213
  if value != self.value:
200
214
  self.update_wires(value)
201
215
 
@@ -221,7 +235,7 @@ class PortIn(PortWire):
221
235
  def __init__(self, parent, name: str, width: int = 1):
222
236
  super().__init__(parent, name, width, output=False)
223
237
 
224
- def set_value(self, value: int | str | None):
238
+ def set_value(self, value: VALUE_TYPE):
225
239
  super().set_value(value)
226
240
  self.parent().update()
227
241
 
@@ -246,16 +260,16 @@ class PortOutDelta(Port):
246
260
  """Set port propagation delay"""
247
261
  self._delay_ns = delay_ns
248
262
 
249
- def set_value(self, value: int | str | None):
263
+ def set_value(self, value: VALUE_TYPE):
250
264
  self.parent().add_event(self, value, self._delay_ns)
251
265
 
252
- def update_port(self, value: int | str | None):
266
+ def update_port(self, value: VALUE_TYPE):
253
267
  """Update the port output and the connected wires"""
254
268
  self.update_wires(value)
255
269
  if self._update_parent:
256
270
  self.parent().update()
257
271
 
258
- def delta_cycle(self, value: int | str | None):
272
+ def delta_cycle(self, value: VALUE_TYPE):
259
273
  """Handle the delta cycle event from the circuit"""
260
274
  self.update_port(value)
261
275
 
@@ -282,11 +296,11 @@ class PortOutImmediate(PortOutDelta):
282
296
  def __init__(self, parent, name: str, width: int = 1):
283
297
  super().__init__(parent, name, width)
284
298
 
285
- def set_value(self, value: int | str | None):
299
+ def set_value(self, value: VALUE_TYPE):
286
300
  self.parent().add_event(self, value, 0)
287
301
  super().update_port(value)
288
302
 
289
- def delta_cycle(self, value: int | str | None):
303
+ def delta_cycle(self, value: VALUE_TYPE):
290
304
  """
291
305
  Do nothing here, the event is just used to updates waves in Circuit class
292
306
  """
@@ -303,7 +317,7 @@ class PortWireBit(PortWire):
303
317
  super().__init__(parent, name, 1, output)
304
318
  self._parent_port = parent_port
305
319
 
306
- def set_value(self, value: int | str | None):
320
+ def set_value(self, value: VALUE_TYPE):
307
321
  super().set_value(value)
308
322
  self._parent_port.update_value_from_bits()
309
323
 
@@ -333,17 +347,20 @@ class PortMultiBitWire(Port):
333
347
  for bit in self._bits:
334
348
  bit.init()
335
349
 
336
- def set_value(self, value: int | str | None):
337
- if value is None or isinstance(value, str):
350
+ def set_value(self, value: VALUE_TYPE):
351
+ if isinstance(value, str):
338
352
  return
339
353
  for bit_id, bit in enumerate(self._bits):
340
354
  bit_val = (value >> bit_id) & 1
341
355
  bit.value = bit_val
342
356
 
343
- def get_wired_ports_recursive(self) -> list[Port]:
344
- all_wired_ports = super().get_wired_ports_recursive()
357
+ def get_wired_ports_recursive(self, processed_ports: Optional[set] = None) -> list[Port]:
358
+ if processed_ports is None:
359
+ processed_ports = set()
360
+
361
+ all_wired_ports = super().get_wired_ports_recursive(processed_ports)
345
362
  for bit in self._bits:
346
- all_wired_ports.extend(bit.get_wired_ports_recursive())
363
+ all_wired_ports.extend(bit.get_wired_ports_recursive(processed_ports))
347
364
  return all_wired_ports
348
365
 
349
366
  def set_driver(self, port: Port | None):
@@ -364,18 +381,17 @@ class PortMultiBitWire(Port):
364
381
 
365
382
  def update_value_from_bits(self):
366
383
  """Update the port with the value of the bits"""
367
- for bit in self._bits:
384
+ value = 0
385
+ for bit_id, bit in enumerate(self._bits):
368
386
  if bit.value == "X":
369
387
  self.update_wires("X")
370
388
  return
371
- value = 0
372
- for bit_id, bit in enumerate(self._bits):
373
389
  value = value | (bit.value << bit_id)
374
390
  self.update_wires(value)
375
391
  # Send event just to update waves
376
392
  self.parent().add_event(self, value, 0)
377
393
 
378
- def delta_cycle(self, value: int | str | None):
394
+ def delta_cycle(self, value: VALUE_TYPE):
379
395
  """
380
396
  Do nothing here, the event passed in 'update_value_from_bits'
381
397
  is just used to updates waves in Circuit class
@@ -33,8 +33,13 @@ class AppFileDataClass:
33
33
 
34
34
  @staticmethod
35
35
  def load(filename):
36
- with open(filename, mode="r", encoding="utf-8") as json_file:
37
- app_filedata_class = AppFileDataClass(**json.load(json_file))
36
+ try:
37
+ with open(filename, mode="r", encoding="utf-8") as json_file:
38
+ app_filedata_class = AppFileDataClass(**json.load(json_file))
39
+ except json.JSONDecodeError as exc:
40
+ raise ValueError(f"Malformed JSON file: {filename} - {exc}") from exc
41
+ except FileNotFoundError as exc:
42
+ raise FileNotFoundError(f"File not found: {filename}") from exc
38
43
  return app_filedata_class
39
44
 
40
45
  def save(self, filename):
@@ -21,9 +21,11 @@ class WireDataClass:
21
21
  dst: str
22
22
 
23
23
  def connect(self, circuit):
24
- src_comp = circuit.get_component(self.src.split(".")[0])
25
- dst_comp = circuit.get_component(self.dst.split(".")[0])
26
- src_comp.port(self.src.split(".")[1]).wire = dst_comp.port(self.dst.split(".")[1])
24
+ src_comp_name, src_port_name = self.src.split(".")
25
+ dst_comp_name, dst_port_name = self.dst.split(".")
26
+ src_comp = circuit.get_component(src_comp_name)
27
+ dst_comp = circuit.get_component(dst_comp_name)
28
+ src_comp.port(src_port_name).wire = dst_comp.port(dst_port_name)
27
29
 
28
30
  @classmethod
29
31
  def list_from_port(cls, src_port):
@@ -92,10 +94,11 @@ class CircuitDataClass:
92
94
  @staticmethod
93
95
  def from_circuit(circuit):
94
96
  dc = CircuitDataClass(name=circuit.name)
95
- for comp in circuit.get_toplevel_components():
97
+ toplevel_components = circuit.get_toplevel_components()
98
+ for comp in toplevel_components:
96
99
  dc.components.append(ComponentDataClass.from_component(comp))
97
100
 
98
- for comp in circuit.get_toplevel_components():
101
+ for comp in toplevel_components:
99
102
  for port in comp.ports:
100
103
  dc.wires.extend(WireDataClass.list_from_port(port))
101
104
 
@@ -108,8 +111,13 @@ class CircuitFileDataClass:
108
111
 
109
112
  @staticmethod
110
113
  def load(filename):
111
- with open(filename, mode="r", encoding="utf-8") as json_file:
112
- dc = CircuitFileDataClass(**json.load(json_file))
114
+ try:
115
+ with open(filename, mode="r", encoding="utf-8") as json_file:
116
+ dc = CircuitFileDataClass(**json.load(json_file))
117
+ except json.JSONDecodeError as exc:
118
+ raise ValueError(f"Malformed JSON file: {filename} - {exc}") from exc
119
+ except FileNotFoundError as exc:
120
+ raise FileNotFoundError(f"File not found: {filename}") from exc
113
121
  return dc
114
122
 
115
123
  def save(self, filename):
digsim/synth/__main__.py CHANGED
@@ -45,7 +45,7 @@ if __name__ == "__main__":
45
45
  subparser = parser.add_subparsers(required=True)
46
46
  synth_parser = subparser.add_parser("synth")
47
47
  synth_parser.add_argument(
48
- "--input-files", "-i", type=str, nargs="*", required=True, help="The verilog input files"
48
+ "--input-files", "-i", type=str, nargs="+", required=True, help="The verilog input files"
49
49
  )
50
50
  synth_parser.add_argument(
51
51
  "--output-file", "-o", type=str, required=True, help="The json output file"
@@ -59,7 +59,7 @@ if __name__ == "__main__":
59
59
  synth_parser.set_defaults(func=_synth_modules)
60
60
  list_parser = subparser.add_parser("list")
61
61
  list_parser.add_argument(
62
- "--input-files", "-i", type=str, nargs="*", required=True, help="The verilog input files"
62
+ "--input-files", "-i", type=str, nargs="+", required=True, help="The verilog input files"
63
63
  )
64
64
  list_parser.set_defaults(func=_list_modules)
65
65
  arguments = parser.parse_args()
@@ -137,7 +137,10 @@ class Synthesis:
137
137
  def synth_to_dict(self, silent=False):
138
138
  """Execute yosys with generated synthesis script and return python dict"""
139
139
  yosys_json = self.synth_to_json(silent)
140
- netlist_dict = json.loads(yosys_json)
140
+ try:
141
+ netlist_dict = json.loads(yosys_json)
142
+ except json.JSONDecodeError as exc:
143
+ raise SynthesisException(f"Malformed JSON output from Yosys: {exc}") from exc
141
144
  return netlist_dict
142
145
 
143
146
  def synth_to_json_file(self, filename, silent=False):
@@ -7,12 +7,15 @@ Module with classes to parse a yosys netlist
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
- from typing import Any, Optional, Union
10
+ from typing import Any, Literal, Optional, Union
11
11
 
12
12
  from pydantic import Field
13
13
  from pydantic.dataclasses import dataclass
14
14
 
15
15
 
16
+ BIT_TYPE = list[Union[int, Literal["X"], Literal["0"], Literal["1"]]]
17
+
18
+
16
19
  @dataclass
17
20
  class NetPort:
18
21
  parent: Union[YosysModule, YosysCell]
@@ -30,7 +33,7 @@ class Nets:
30
33
  @dataclass
31
34
  class YosysPort:
32
35
  direction: str
33
- bits: list[Union[int, str]]
36
+ bits: BIT_TYPE
34
37
 
35
38
  @property
36
39
  def is_output(self):
@@ -46,13 +49,16 @@ class YosysPort:
46
49
  class YosysCell:
47
50
  type: str
48
51
  port_directions: dict[str, str] = Field(default_factory=dict)
49
- connections: dict[str, list[Union[str, int]]] = Field(default_factory=dict)
52
+ connections: dict[str, BIT_TYPE] = Field(default_factory=dict)
50
53
  hide_name: int = 0
51
54
  parameters: dict[str, Any] = Field(default_factory=dict)
52
55
  attributes: dict[str, Any] = Field(default_factory=dict)
53
56
 
54
57
  def get_nets(self, name, nets):
55
58
  for port_name, net_list in self.connections.items():
59
+ if not net_list:
60
+ # Handle empty net_list, e.g., by skipping or raising an error
61
+ continue
56
62
  net = net_list[0]
57
63
  port = NetPort(parent=self, parent_name=name, name=port_name)
58
64
  if self.port_directions[port_name] == "input":
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: digsim-logic-simulator
3
- Version: 0.12.0
3
+ Version: 0.13.0
4
4
  Summary: Interactive Digital Logic Simulator
5
5
  Author-email: Fredrik Andersson <freand@gmail.com>
6
6
  Maintainer-email: Fredrik Andersson <freand@gmail.com>