sim: split into base, core, and engines.
authorwhitequark <whitequark@whitequark.org>
Thu, 27 Aug 2020 10:17:02 +0000 (10:17 +0000)
committerwhitequark <whitequark@whitequark.org>
Thu, 27 Aug 2020 11:52:31 +0000 (11:52 +0000)
Before this commit, each simulation engine (which is only pysim at
the moment, but also cxxsim soon) was a subclass of SimulatorCore,
and every simulation engine module would essentially duplicate
the complete structure of a simulator, with code partially shared.

This was a really bad idea: it was inconvenient to use, with
downstream code having to branch between e.g. PySettle and CxxSettle;
it had no well-defined external interface; it had multiple virtually
identical entry points; and it had no separation between simulation
algorithms and glue code.

This commit completely rearranges simulation code.
  1. sim._base defines internal simulation interfaces. The clarity of
     these internal interfaces is important because simulation
     engines mix and match components to provide a consistent API
     regardless of the chosen engine.
  2. sim.core defines the external simulation interface: the commands
     and the simulator facade. The facade provides a single entry
     point and, when possible, validates or lowers user input.
     It also imports built-in simulation engines by their symbolic
     name, avoiding eager imports of pyvcd or ctypes.
  3. sim.xxxsim (currently, only sim.pysim) defines the simulator
     implementation: time and state management, process scheduling,
     and waveform dumping.

The new simulator structure has none of the downsides of the old one.

See #324.

19 files changed:
examples/basic/ctr_en.py
nmigen/back/pysim.py
nmigen/compat/sim/__init__.py
nmigen/sim/__init__.py
nmigen/sim/_base.py [new file with mode: 0644]
nmigen/sim/_cmds.py [deleted file]
nmigen/sim/_core.py [deleted file]
nmigen/sim/_pyclock.py
nmigen/sim/_pycoro.py
nmigen/sim/_pyrtl.py
nmigen/sim/core.py [new file with mode: 0644]
nmigen/sim/pysim.py
nmigen/vendor/lattice_machxo2.py
tests/test_lib_cdc.py
tests/test_lib_coding.py
tests/test_lib_fifo.py
tests/test_lib_io.py
tests/test_lib_scheduler.py
tests/test_sim.py

index becc7c9210ec471f7a3aaedd818430ba6e786142..d5e327911dbef760c6c23a35b63d0a63596ef92c 100644 (file)
@@ -1,5 +1,6 @@
 from nmigen import *
-from nmigen.back import rtlil, verilog, pysim
+from nmigen.sim import *
+from nmigen.back import rtlil, verilog
 
 
 class Counter(Elaboratable):
@@ -19,7 +20,7 @@ ctr = Counter(width=16)
 
 print(verilog.convert(ctr, ports=[ctr.o, ctr.en]))
 
-sim = pysim.Simulator(ctr)
+sim = Simulator(ctr)
 sim.add_clock(1e-6)
 def ce_proc():
     yield; yield; yield
index 039265ac6f75112aec2b3eb636eee19466d899c3..c35f07365c5a9f347b32e186b309d5c1c3f07be7 100644 (file)
@@ -1,11 +1,11 @@
 import warnings
 
-from ..sim.pysim import *
+from ..sim import *
 
 
 __all__ = ["Settle", "Delay", "Tick", "Passive", "Active", "Simulator"]
 
 
 # TODO(nmigen-0.4): remove
-warnings.warn("instead of back.pysim, use sim.pysim",
+warnings.warn("instead of nmigen.back.pysim.*, use nmigen.sim.*",
               DeprecationWarning, stacklevel=2)
index b2cc87e9f519c904bece329c582b4225d2333b6f..4c3d063844364cc62e9c08933ad21818be436c47 100644 (file)
@@ -2,8 +2,8 @@ import functools
 import inspect
 from collections.abc import Iterable
 from ...hdl.cd import ClockDomain
-from ...back.pysim import *
 from ...hdl.ir import Fragment
+from ...sim import *
 
 
 __all__ = ["run_simulation", "passive"]
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..c239c52cad001d40b7b35d3c888b762b0a35f3d5 100644 (file)
@@ -0,0 +1,4 @@
+from .core import *
+
+
+__all__ = ["Settle", "Delay", "Tick", "Passive", "Active", "Simulator"]
diff --git a/nmigen/sim/_base.py b/nmigen/sim/_base.py
new file mode 100644 (file)
index 0000000..ee1061f
--- /dev/null
@@ -0,0 +1,67 @@
+__all__ = ["BaseProcess", "BaseSignalState", "BaseSimulation", "BaseEngine"]
+
+
+class BaseProcess:
+    __slots__ = ()
+
+    def __init__(self):
+        self.reset()
+
+    def reset(self):
+        self.runnable = False
+        self.passive  = True
+
+    def run(self):
+        raise NotImplementedError
+
+
+class BaseSignalState:
+    __slots__ = ()
+
+    signal = NotImplemented
+
+    curr = NotImplemented
+    next = NotImplemented
+
+    def set(self, value):
+        raise NotImplementedError
+
+
+class BaseSimulation:
+    def reset(self):
+        raise NotImplementedError
+
+    def get_signal(self, signal):
+        raise NotImplementedError
+
+    slots = NotImplemented
+
+    def add_trigger(self, process, signal, *, trigger=None):
+        raise NotImplementedError
+
+    def remove_trigger(self, process, signal):
+        raise NotImplementedError
+
+    def wait_interval(self, process, interval):
+        raise NotImplementedError
+
+
+class BaseEngine:
+    def add_coroutine_process(self, process, *, default_cmd):
+        raise NotImplementedError
+
+    def add_clock_process(self, clock, *, phase, period):
+        raise NotImplementedError
+
+    def reset(self):
+        raise NotImplementedError
+
+    @property
+    def now(self):
+        raise NotImplementedError
+
+    def advance(self):
+        raise NotImplementedError
+
+    def write_vcd(self, *, vcd_file, gtkw_file, traces):
+        raise NotImplementedError
diff --git a/nmigen/sim/_cmds.py b/nmigen/sim/_cmds.py
deleted file mode 100644 (file)
index a1cea3e..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-from ..hdl.cd import *
-
-
-__all__ = ["Settle", "Delay", "Tick", "Passive", "Active"]
-
-
-class Command:
-    pass
-
-
-class Settle(Command):
-    def __repr__(self):
-        return "(settle)"
-
-
-class Delay(Command):
-    def __init__(self, interval=None):
-        self.interval = None if interval is None else float(interval)
-
-    def __repr__(self):
-        if self.interval is None:
-            return "(delay ε)"
-        else:
-            return "(delay {:.3}us)".format(self.interval * 1e6)
-
-
-class Tick(Command):
-    def __init__(self, domain="sync"):
-        if not isinstance(domain, (str, ClockDomain)):
-            raise TypeError("Domain must be a string or a ClockDomain instance, not {!r}"
-                            .format(domain))
-        assert domain != "comb"
-        self.domain = domain
-
-    def __repr__(self):
-        return "(tick {})".format(self.domain)
-
-
-class Passive(Command):
-    def __repr__(self):
-        return "(passive)"
-
-
-class Active(Command):
-    def __repr__(self):
-        return "(active)"
diff --git a/nmigen/sim/_core.py b/nmigen/sim/_core.py
deleted file mode 100644 (file)
index e1af946..0000000
+++ /dev/null
@@ -1,63 +0,0 @@
-__all__ = ["Process", "Timeline"]
-
-
-class Process:
-    def __init__(self, *, is_comb):
-        self.is_comb  = is_comb
-
-        self.reset()
-
-    def reset(self):
-        self.runnable = self.is_comb
-        self.passive  = True
-
-    def run(self):
-        raise NotImplementedError
-
-
-class Timeline:
-    def __init__(self):
-        self.now = 0.0
-        self.deadlines = dict()
-
-    def reset(self):
-        self.now = 0.0
-        self.deadlines.clear()
-
-    def at(self, run_at, process):
-        assert process not in self.deadlines
-        self.deadlines[process] = run_at
-
-    def delay(self, delay_by, process):
-        if delay_by is None:
-            run_at = self.now
-        else:
-            run_at = self.now + delay_by
-        self.at(run_at, process)
-
-    def advance(self):
-        nearest_processes = set()
-        nearest_deadline = None
-        for process, deadline in self.deadlines.items():
-            if deadline is None:
-                if nearest_deadline is not None:
-                    nearest_processes.clear()
-                nearest_processes.add(process)
-                nearest_deadline = self.now
-                break
-            elif nearest_deadline is None or deadline <= nearest_deadline:
-                assert deadline >= self.now
-                if nearest_deadline is not None and deadline < nearest_deadline:
-                    nearest_processes.clear()
-                nearest_processes.add(process)
-                nearest_deadline = deadline
-
-        if not nearest_processes:
-            return False
-
-        for process in nearest_processes:
-            process.runnable = True
-            del self.deadlines[process]
-        self.now = nearest_deadline
-
-        return True
index 3e1e6abcd2ccbe355ebb9821b9d88ee4bc838c66..b3608023ceb55f15ebbb0638df73818a647ed0ea 100644 (file)
@@ -1,12 +1,12 @@
 import inspect
 
-from ._core import Process
+from ._base import BaseProcess
 
 
 __all__ = ["PyClockProcess"]
 
 
-class PyClockProcess(Process):
+class PyClockProcess(BaseProcess):
     def __init__(self, state, signal, *, phase, period):
         assert len(signal) == 1
 
@@ -20,16 +20,17 @@ class PyClockProcess(Process):
     def reset(self):
         self.runnable = True
         self.passive = True
+
         self.initial = True
 
     def run(self):
+        self.runnable = False
+
         if self.initial:
             self.initial = False
-            self.state.timeline.delay(self.phase, self)
+            self.state.wait_interval(self, self.phase)
 
         else:
             clk_state = self.state.slots[self.slot]
             clk_state.set(not clk_state.curr)
-            self.state.timeline.delay(self.period / 2, self)
-
-        self.runnable = False
+            self.state.wait_interval(self, self.period / 2)
index dbd7601d73d176996276dee8530fce8608d42e04..554187c50ce09ae60246a82ef84ae6ee238e89fa 100644 (file)
@@ -2,15 +2,15 @@ import inspect
 
 from ..hdl import *
 from ..hdl.ast import Statement, SignalSet
-from ._cmds import *
-from ._core import Process
+from .core import Tick, Settle, Delay, Passive, Active
+from ._base import BaseProcess
 from ._pyrtl import _ValueCompiler, _RHSValueCompiler, _StatementCompiler
 
 
 __all__ = ["PyCoroProcess"]
 
 
-class PyCoroProcess(Process):
+class PyCoroProcess(BaseProcess):
     def __init__(self, state, domains, constructor, *, default_cmd=None):
         self.state = state
         self.domains = domains
@@ -22,6 +22,7 @@ class PyCoroProcess(Process):
     def reset(self):
         self.runnable = True
         self.passive = False
+
         self.coroutine = self.constructor()
         self.exec_locals = {
             "slots": self.state.slots,
@@ -90,11 +91,11 @@ class PyCoroProcess(Process):
                     return
 
                 elif type(command) is Settle:
-                    self.state.timeline.delay(None, self)
+                    self.state.wait_interval(self, None)
                     return
 
                 elif type(command) is Delay:
-                    self.state.timeline.delay(command.interval, self)
+                    self.state.wait_interval(self, command.interval)
                     return
 
                 elif type(command) is Passive:
index baf9a5bbc24724633cfe502d8164e53be191d4dd..d7b432606134b8ea21aca1b1c53636027a75d44d 100644 (file)
@@ -5,14 +5,23 @@ from contextlib import contextmanager
 from ..hdl import *
 from ..hdl.ast import SignalSet
 from ..hdl.xfrm import ValueVisitor, StatementVisitor, LHSGroupFilter
-from ._core import *
+from ._base import BaseProcess
 
 
 __all__ = ["PyRTLProcess"]
 
 
-class PyRTLProcess(Process):
-    pass
+class PyRTLProcess(BaseProcess):
+    __slots__ = ("is_comb", "runnable", "passive", "run")
+
+    def __init__(self, *, is_comb):
+        self.is_comb  = is_comb
+
+        self.reset()
+
+    def reset(self):
+        self.runnable = self.is_comb
+        self.passive  = True
 
 
 class _PythonEmitter:
diff --git a/nmigen/sim/core.py b/nmigen/sim/core.py
new file mode 100644 (file)
index 0000000..886cea1
--- /dev/null
@@ -0,0 +1,206 @@
+import inspect
+
+from .._utils import deprecated
+from ..hdl.cd import *
+from ..hdl.ir import *
+from ._base import BaseEngine
+
+
+__all__ = ["Settle", "Delay", "Tick", "Passive", "Active", "Simulator"]
+
+
+class Command:
+    pass
+
+
+class Settle(Command):
+    def __repr__(self):
+        return "(settle)"
+
+
+class Delay(Command):
+    def __init__(self, interval=None):
+        self.interval = None if interval is None else float(interval)
+
+    def __repr__(self):
+        if self.interval is None:
+            return "(delay ε)"
+        else:
+            return "(delay {:.3}us)".format(self.interval * 1e6)
+
+
+class Tick(Command):
+    def __init__(self, domain="sync"):
+        if not isinstance(domain, (str, ClockDomain)):
+            raise TypeError("Domain must be a string or a ClockDomain instance, not {!r}"
+                            .format(domain))
+        assert domain != "comb"
+        self.domain = domain
+
+    def __repr__(self):
+        return "(tick {})".format(self.domain)
+
+
+class Passive(Command):
+    def __repr__(self):
+        return "(passive)"
+
+
+class Active(Command):
+    def __repr__(self):
+        return "(active)"
+
+
+class Simulator:
+    def __init__(self, fragment, *, engine="pysim"):
+        if isinstance(engine, type) and issubclass(engine, BaseEngine):
+            pass
+        elif engine == "pysim":
+            from .pysim import PySimEngine
+            engine = PySimEngine
+        else:
+            raise TypeError("Value '{!r}' is not a simulation engine class or "
+                            "a simulation engine name"
+                            .format(engine))
+
+        self._fragment = Fragment.get(fragment, platform=None).prepare()
+        self._engine   = engine(self._fragment)
+        self._clocked  = set()
+
+    def _check_process(self, process):
+        if not (inspect.isgeneratorfunction(process) or inspect.iscoroutinefunction(process)):
+            raise TypeError("Cannot add a process {!r} because it is not a generator function"
+                            .format(process))
+        return process
+
+    def add_process(self, process):
+        process = self._check_process(process)
+        def wrapper():
+            # Only start a bench process after comb settling, so that the reset values are correct.
+            yield Settle()
+            yield from process()
+        self._engine.add_coroutine_process(wrapper, default_cmd=None)
+
+    def add_sync_process(self, process, *, domain="sync"):
+        process = self._check_process(process)
+        def wrapper():
+            # Only start a sync process after the first clock edge (or reset edge, if the domain
+            # uses an asynchronous reset). This matches the behavior of synchronous FFs.
+            yield Tick(domain)
+            yield from process()
+        self._engine.add_coroutine_process(wrapper, default_cmd=Tick(domain))
+
+    def add_clock(self, period, *, phase=None, domain="sync", if_exists=False):
+        """Add a clock process.
+
+        Adds a process that drives the clock signal of ``domain`` at a 50% duty cycle.
+
+        Arguments
+        ---------
+        period : float
+            Clock period. The process will toggle the ``domain`` clock signal every ``period / 2``
+            seconds.
+        phase : None or float
+            Clock phase. The process will wait ``phase`` seconds before the first clock transition.
+            If not specified, defaults to ``period / 2``.
+        domain : str or ClockDomain
+            Driven clock domain. If specified as a string, the domain with that name is looked up
+            in the root fragment of the simulation.
+        if_exists : bool
+            If ``False`` (the default), raise an error if the driven domain is specified as
+            a string and the root fragment does not have such a domain. If ``True``, do nothing
+            in this case.
+        """
+        if isinstance(domain, ClockDomain):
+            pass
+        elif domain in self._fragment.domains:
+            domain = self._fragment.domains[domain]
+        elif if_exists:
+            return
+        else:
+            raise ValueError("Domain {!r} is not present in simulation"
+                             .format(domain))
+        if domain in self._clocked:
+            raise ValueError("Domain {!r} already has a clock driving it"
+                             .format(domain.name))
+
+        if phase is None:
+            # By default, delay the first edge by half period. This causes any synchronous activity
+            # to happen at a non-zero time, distinguishing it from the reset values in the waveform
+            # viewer.
+            phase = period / 2
+        self._engine.add_clock_process(domain.clk, phase=phase, period=period)
+        self._clocked.add(domain)
+
+    def reset(self):
+        """Reset the simulation.
+
+        Assign the reset value to every signal in the simulation, and restart every user process.
+        """
+        self._engine.reset()
+
+    # TODO(nmigen-0.4): replace with _real_step
+    @deprecated("instead of `sim.step()`, use `sim.advance()`")
+    def step(self):
+        return self.advance()
+
+    def advance(self):
+        """Advance the simulation.
+
+        Run every process and commit changes until a fixed point is reached, then advance time
+        to the closest deadline (if any). If there is an unstable combinatorial loop,
+        this function will never return.
+
+        Returns ``True`` if there are any active processes, ``False`` otherwise.
+        """
+        return self._engine.advance()
+
+    def run(self):
+        """Run the simulation while any processes are active.
+
+        Processes added with :meth:`add_process` and :meth:`add_sync_process` are initially active,
+        and may change their status using the ``yield Passive()`` and ``yield Active()`` commands.
+        Processes compiled from HDL and added with :meth:`add_clock` are always passive.
+        """
+        while self.advance():
+            pass
+
+    def run_until(self, deadline, *, run_passive=False):
+        """Run the simulation until it advances to ``deadline``.
+
+        If ``run_passive`` is ``False``, the simulation also stops when there are no active
+        processes, similar to :meth:`run`. Otherwise, the simulation will stop only after it
+        advances to or past ``deadline``.
+
+        If the simulation stops advancing, this function will never return.
+        """
+        assert self._engine.now <= deadline
+        while (self.advance() or run_passive) and self._engine.now < deadline:
+            pass
+
+    def write_vcd(self, vcd_file, gtkw_file=None, *, traces=()):
+        """Write waveforms to a Value Change Dump file, optionally populating a GTKWave save file.
+
+        This method returns a context manager. It can be used as: ::
+
+            sim = Simulator(frag)
+            sim.add_clock(1e-6)
+            with sim.write_vcd("dump.vcd", "dump.gtkw"):
+                sim.run_until(1e-3)
+
+        Arguments
+        ---------
+        vcd_file : str or file-like object
+            Verilog Value Change Dump file or filename.
+        gtkw_file : str or file-like object
+            GTKWave save file or filename.
+        traces : iterable of Signal
+            Signals to display traces for.
+        """
+        if self._engine.now != 0.0:
+            for file in (vcd_file, gtkw_file):
+                if hasattr(file, "close"):
+                    file.close()
+            raise ValueError("Cannot start writing waveforms after advancing simulation time")
+
+        return self._engine.write_vcd(vcd_file=vcd_file, gtkw_file=gtkw_file, traces=traces)
index 847a5a1e6c0229f1ad48803808e1aa4262403b9d..c3fc1769a6a79ab4508f7caacc159d947bbf80ee 100644 (file)
@@ -1,20 +1,17 @@
 from contextlib import contextmanager
 import itertools
-import inspect
 from vcd import VCDWriter
 from vcd.gtkw import GTKWSave
 
-from .._utils import deprecated
 from ..hdl import *
 from ..hdl.ast import SignalDict
-from ._cmds import *
-from ._core import *
+from ._base import *
 from ._pyrtl import _FragmentCompiler
 from ._pycoro import PyCoroProcess
 from ._pyclock import PyClockProcess
 
 
-__all__ = ["Settle", "Delay", "Tick", "Passive", "Active", "Simulator"]
+__all__ = ["PySimEngine"]
 
 
 class _NameExtractor:
@@ -49,15 +46,7 @@ class _NameExtractor:
         return self.names
 
 
-class _WaveformWriter:
-    def update(self, timestamp, signal, value):
-        raise NotImplementedError # :nocov:
-
-    def close(self, timestamp):
-        raise NotImplementedError # :nocov:
-
-
-class _VCDWaveformWriter(_WaveformWriter):
+class _VCDWriter:
     @staticmethod
     def timestamp_to_vcd(timestamp):
         return timestamp * (10 ** 10) # 1/(100 ps)
@@ -162,7 +151,55 @@ class _VCDWaveformWriter(_WaveformWriter):
             self.gtkw_file.close()
 
 
-class _SignalState:
+class _Timeline:
+    def __init__(self):
+        self.now = 0.0
+        self.deadlines = dict()
+
+    def reset(self):
+        self.now = 0.0
+        self.deadlines.clear()
+
+    def at(self, run_at, process):
+        assert process not in self.deadlines
+        self.deadlines[process] = run_at
+
+    def delay(self, delay_by, process):
+        if delay_by is None:
+            run_at = self.now
+        else:
+            run_at = self.now + delay_by
+        self.at(run_at, process)
+
+    def advance(self):
+        nearest_processes = set()
+        nearest_deadline = None
+        for process, deadline in self.deadlines.items():
+            if deadline is None:
+                if nearest_deadline is not None:
+                    nearest_processes.clear()
+                nearest_processes.add(process)
+                nearest_deadline = self.now
+                break
+            elif nearest_deadline is None or deadline <= nearest_deadline:
+                assert deadline >= self.now
+                if nearest_deadline is not None and deadline < nearest_deadline:
+                    nearest_processes.clear()
+                nearest_processes.add(process)
+                nearest_deadline = deadline
+
+        if not nearest_processes:
+            return False
+
+        for process in nearest_processes:
+            process.runnable = True
+            del self.deadlines[process]
+        self.now = nearest_deadline
+
+        return True
+
+
+class _PySignalState(BaseSignalState):
     __slots__ = ("signal", "curr", "next", "waiters", "pending")
 
     def __init__(self, signal, pending):
@@ -189,9 +226,9 @@ class _SignalState:
         return awoken_any
 
 
-class _SimulatorState:
+class _PySimulation(BaseSimulation):
     def __init__(self):
-        self.timeline = Timeline()
+        self.timeline = _Timeline()
         self.signals  = SignalDict()
         self.slots    = []
         self.pending  = set()
@@ -207,7 +244,7 @@ class _SimulatorState:
             return self.signals[signal]
         except KeyError:
             index = len(self.slots)
-            self.slots.append(_SignalState(signal, self.pending))
+            self.slots.append(_PySignalState(signal, self.pending))
             self.signals[signal] = index
             return index
 
@@ -222,6 +259,9 @@ class _SimulatorState:
         assert process in self.slots[index].waiters
         del self.slots[index].waiters[process]
 
+    def wait_interval(self, process, interval):
+        self.timeline.delay(interval, process)
+
     def commit(self):
         converged = True
         for signal_state in self.pending:
@@ -231,98 +271,29 @@ class _SimulatorState:
         return converged
 
 
-class Simulator:
+class PySimEngine(BaseEngine):
     def __init__(self, fragment):
-        self._state = _SimulatorState()
-        self._fragment = Fragment.get(fragment, platform=None).prepare()
-        self._processes = _FragmentCompiler(self._state)(self._fragment)
-        self._clocked = set()
-        self._waveform_writers = []
+        self._state = _PySimulation()
+        self._timeline = self._state.timeline
 
-    def _check_process(self, process):
-        if not (inspect.isgeneratorfunction(process) or inspect.iscoroutinefunction(process)):
-            raise TypeError("Cannot add a process {!r} because it is not a generator function"
-                            .format(process))
-        return process
+        self._fragment = fragment
+        self._processes = _FragmentCompiler(self._state)(self._fragment)
+        self._vcd_writers = []
 
-    def _add_coroutine_process(self, process, *, default_cmd):
+    def add_coroutine_process(self, process, *, default_cmd):
         self._processes.add(PyCoroProcess(self._state, self._fragment.domains, process,
                                           default_cmd=default_cmd))
 
-    def add_process(self, process):
-        process = self._check_process(process)
-        def wrapper():
-            # Only start a bench process after comb settling, so that the reset values are correct.
-            yield Settle()
-            yield from process()
-        self._add_coroutine_process(wrapper, default_cmd=None)
-
-    def add_sync_process(self, process, *, domain="sync"):
-        process = self._check_process(process)
-        def wrapper():
-            # Only start a sync process after the first clock edge (or reset edge, if the domain
-            # uses an asynchronous reset). This matches the behavior of synchronous FFs.
-            yield Tick(domain)
-            yield from process()
-        return self._add_coroutine_process(wrapper, default_cmd=Tick(domain))
-
-    def add_clock(self, period, *, phase=None, domain="sync", if_exists=False):
-        """Add a clock process.
-
-        Adds a process that drives the clock signal of ``domain`` at a 50% duty cycle.
-
-        Arguments
-        ---------
-        period : float
-            Clock period. The process will toggle the ``domain`` clock signal every ``period / 2``
-            seconds.
-        phase : None or float
-            Clock phase. The process will wait ``phase`` seconds before the first clock transition.
-            If not specified, defaults to ``period / 2``.
-        domain : str or ClockDomain
-            Driven clock domain. If specified as a string, the domain with that name is looked up
-            in the root fragment of the simulation.
-        if_exists : bool
-            If ``False`` (the default), raise an error if the driven domain is specified as
-            a string and the root fragment does not have such a domain. If ``True``, do nothing
-            in this case.
-        """
-        if isinstance(domain, ClockDomain):
-            pass
-        elif domain in self._fragment.domains:
-            domain = self._fragment.domains[domain]
-        elif if_exists:
-            return
-        else:
-            raise ValueError("Domain {!r} is not present in simulation"
-                             .format(domain))
-        if domain in self._clocked:
-            raise ValueError("Domain {!r} already has a clock driving it"
-                             .format(domain.name))
-
-        if phase is None:
-            # By default, delay the first edge by half period. This causes any synchronous activity
-            # to happen at a non-zero time, distinguishing it from the reset values in the waveform
-            # viewer.
-            phase = period / 2
-        self._processes.add(PyClockProcess(self._state, domain.clk, phase=phase, period=period))
-        self._clocked.add(domain)
+    def add_clock_process(self, clock, *, phase, period):
+        self._processes.add(PyClockProcess(self._state, clock,
+                                           phase=phase, period=period))
 
     def reset(self):
-        """Reset the simulation.
-
-        Assign the reset value to every signal in the simulation, and restart every user process.
-        """
         self._state.reset()
         for process in self._processes:
             process.reset()
 
-    def _real_step(self):
-        """Step the simulation.
-
-        Run every process and commit changes until a fixed point is reached. If there is
-        an unstable combinatorial loop, this function will never return.
-        """
+    def _step(self):
         # Performs the two phases of a delta cycle in a loop:
         converged = False
         while not converged:
@@ -332,86 +303,30 @@ class Simulator:
                     process.runnable = False
                     process.run()
 
-            for waveform_writer in self._waveform_writers:
+            for vcd_writer in self._vcd_writers:
                 for signal_state in self._state.pending:
-                    waveform_writer.update(self._state.timeline.now,
+                    vcd_writer.update(self._timeline.now,
                         signal_state.signal, signal_state.next)
 
             # 2. commit: apply every queued signal change, waking up any waiting processes
             converged = self._state.commit()
 
-    # TODO(nmigen-0.4): replace with _real_step
-    @deprecated("instead of `sim.step()`, use `sim.advance()`")
-    def step(self):
-        return self.advance()
-
     def advance(self):
-        """Advance the simulation.
-
-        Run every process and commit changes until a fixed point is reached, then advance time
-        to the closest deadline (if any). If there is an unstable combinatorial loop,
-        this function will never return.
-
-        Returns ``True`` if there are any active processes, ``False`` otherwise.
-        """
-        self._real_step()
-        self._state.timeline.advance()
+        self._step()
+        self._timeline.advance()
         return any(not process.passive for process in self._processes)
 
-    def run(self):
-        """Run the simulation while any processes are active.
-
-        Processes added with :meth:`add_process` and :meth:`add_sync_process` are initially active,
-        and may change their status using the ``yield Passive()`` and ``yield Active()`` commands.
-        Processes compiled from HDL and added with :meth:`add_clock` are always passive.
-        """
-        while self.advance():
-            pass
-
-    def run_until(self, deadline, *, run_passive=False):
-        """Run the simulation until it advances to ``deadline``.
-
-        If ``run_passive`` is ``False``, the simulation also stops when there are no active
-        processes, similar to :meth:`run`. Otherwise, the simulation will stop only after it
-        advances to or past ``deadline``.
-
-        If the simulation stops advancing, this function will never return.
-        """
-        assert self._state.timeline.now <= deadline
-        while (self.advance() or run_passive) and self._state.timeline.now < deadline:
-            pass
+    @property
+    def now(self):
+        return self._timeline.now
 
     @contextmanager
-    def write_vcd(self, vcd_file, gtkw_file=None, *, traces=()):
-        """Write waveforms to a Value Change Dump file, optionally populating a GTKWave save file.
-
-        This method returns a context manager. It can be used as: ::
-
-            sim = Simulator(frag)
-            sim.add_clock(1e-6)
-            with sim.write_vcd("dump.vcd", "dump.gtkw"):
-                sim.run_until(1e-3)
-
-        Arguments
-        ---------
-        vcd_file : str or file-like object
-            Verilog Value Change Dump file or filename.
-        gtkw_file : str or file-like object
-            GTKWave save file or filename.
-        traces : iterable of Signal
-            Signals to display traces for.
-        """
-        if self._state.timeline.now != 0.0:
-            for file in (vcd_file, gtkw_file):
-                if hasattr(file, "close"):
-                    file.close()
-            raise ValueError("Cannot start writing waveforms after advancing simulation time")
-
-        waveform_writer = _VCDWaveformWriter(self._fragment,
+    def write_vcd(self, *, vcd_file, gtkw_file, traces):
+        vcd_writer = _VCDWriter(self._fragment,
             vcd_file=vcd_file, gtkw_file=gtkw_file, traces=traces)
         try:
-            self._waveform_writers.append(waveform_writer)
+            self._vcd_writers.append(vcd_writer)
             yield
         finally:
-            waveform_writer.close(self._state.timeline.now)
-            self._waveform_writers.remove(waveform_writer)
+            vcd_writer.close(self._timeline.now)
+            self._vcd_writers.remove(vcd_writer)
index 1d01506ad5a2bc4d6311b9e501c80629a33e4522..b9fadab2baaa0a44d7ee3432ab62d5c27b5d17cc 100644 (file)
@@ -7,5 +7,5 @@ __all__ = ["LatticeMachXO2Platform"]
 
 
 # TODO(nmigen-0.4): remove
-warnings.warn("instead of vendor.lattice_machxo2, use vendor.lattice_machxo_2_3l",
+warnings.warn("instead of nmigen.vendor.lattice_machxo2, use nmigen.vendor.lattice_machxo_2_3l",
               DeprecationWarning, stacklevel=2)
index f29052a1be7159d1f19b2fa984c288a291fa7fe1..1ada46d2df331fc3e0eb501ec343c75ade4cb86c 100644 (file)
@@ -1,7 +1,7 @@
 # nmigen: UnusedElaboratable=no
 
 from nmigen.hdl import *
-from nmigen.back.pysim import *
+from nmigen.sim import *
 from nmigen.lib.cdc import *
 
 from .utils import *
index c914e9ef3820954ed3d70f9811602709ea5e0a85..f981a13c5e5d1f1873190ac383e39343543542ee 100644 (file)
@@ -1,6 +1,6 @@
 from nmigen.hdl import *
 from nmigen.asserts import *
-from nmigen.back.pysim import *
+from nmigen.sim import *
 from nmigen.lib.coding import *
 
 from .utils import *
index 83dc825313ca6d153f100f88c3d6973aa631afd7..4722318ed2aade3bd66b17b87aaff1100cc0b2bc 100644 (file)
@@ -2,7 +2,7 @@
 
 from nmigen.hdl import *
 from nmigen.asserts import *
-from nmigen.back.pysim import *
+from nmigen.sim import *
 from nmigen.lib.fifo import *
 
 from .utils import *
index 234df1d0fd5dda3a58784988742a20d4290ff901..597065013142a143d8eded57e24e3b8f69bb7837 100644 (file)
@@ -1,6 +1,6 @@
 from nmigen.hdl import *
 from nmigen.hdl.rec import *
-from nmigen.back.pysim import *
+from nmigen.sim import *
 from nmigen.lib.io import *
 
 from .utils import *
index 3128ad17ad1c268afd94fa413ebf9cb6979fa8ae..a3780ed9a2d0b70387b72c6f2e6fbca3f1c1cf47 100644 (file)
@@ -4,7 +4,7 @@ import unittest
 
 from nmigen.hdl import *
 from nmigen.asserts import *
-from nmigen.sim.pysim import *
+from nmigen.sim import *
 from nmigen.lib.scheduler import *
 
 from .utils import *
index 6c125ba418c5590f31f06d12dbe5dda88bff4f59..bb806f26574fc9a21bcc12ac10a2533b461fba1d 100644 (file)
@@ -8,7 +8,7 @@ from nmigen.hdl.mem import *
 from nmigen.hdl.rec import *
 from nmigen.hdl.dsl import  *
 from nmigen.hdl.ir import *
-from nmigen.back.pysim import *
+from nmigen.sim import *
 
 from .utils import *