hdl._dsl: raise an error when modifying an already-elaborated Module.

This renames the `FrozenMemory` exception to `AlreadyElaborated`
and reuses it for modules.

Fixes #1350.
This commit is contained in:
Wanda 2024-05-08 01:15:35 +02:00 committed by Catherine
parent 1d03c3498d
commit 77dab7884c
8 changed files with 73 additions and 28 deletions

View file

@ -6,9 +6,9 @@ from ._ast import Format, Print, Assert, Assume, Cover
from ._ast import IOValue, IOPort from ._ast import IOValue, IOPort
from ._dsl import Module from ._dsl import Module
from ._cd import DomainError, ClockDomain from ._cd import DomainError, ClockDomain
from ._ir import UnusedElaboratable, Elaboratable, DriverConflict, Fragment from ._ir import AlreadyElaborated, UnusedElaboratable, Elaboratable, DriverConflict, Fragment
from ._ir import Instance, IOBufferInstance from ._ir import Instance, IOBufferInstance
from ._mem import FrozenMemory, MemoryData, MemoryInstance, Memory, ReadPort, WritePort, DummyPort from ._mem import MemoryData, MemoryInstance, Memory, ReadPort, WritePort, DummyPort
from ._nir import CombinationalCycle from ._nir import CombinationalCycle
from ._rec import Record from ._rec import Record
from ._xfrm import DomainRenamer, ResetInserter, EnableInserter from ._xfrm import DomainRenamer, ResetInserter, EnableInserter
@ -27,12 +27,12 @@ __all__ = [
# _cd # _cd
"DomainError", "ClockDomain", "DomainError", "ClockDomain",
# _ir # _ir
"UnusedElaboratable", "Elaboratable", "DriverConflict", "Fragment", "AlreadyElaborated", "UnusedElaboratable", "Elaboratable", "DriverConflict", "Fragment",
"Instance", "IOBufferInstance", "Instance", "IOBufferInstance",
# _nir # _nir
"CombinationalCycle", "CombinationalCycle",
# _mem # _mem
"FrozenMemory", "MemoryData", "MemoryInstance", "Memory", "ReadPort", "WritePort", "DummyPort", "MemoryData", "MemoryInstance", "Memory", "ReadPort", "WritePort", "DummyPort",
# _rec # _rec
"Record", "Record",
# _xfrm # _xfrm

View file

@ -288,8 +288,11 @@ class Module(_ModuleBuilderRoot, Elaboratable):
self._domains = {} self._domains = {}
self._generated = {} self._generated = {}
self._src_loc = tracer.get_src_loc() self._src_loc = tracer.get_src_loc()
self._frozen = False
def _check_context(self, construct, context): def _check_context(self, construct, context):
if self._frozen:
raise AlreadyElaborated(f"Cannot modify a module that has already been elaborated")
if self._ctrl_context != context: if self._ctrl_context != context:
if self._ctrl_context is None: if self._ctrl_context is None:
raise SyntaxError("{} is not permitted outside of {}" raise SyntaxError("{} is not permitted outside of {}"
@ -616,6 +619,9 @@ class Module(_ModuleBuilderRoot, Elaboratable):
src_loc=src_loc)) src_loc=src_loc))
def _add_statement(self, assigns, domain, depth): def _add_statement(self, assigns, domain, depth):
if self._frozen:
raise AlreadyElaborated(f"Cannot modify a module that has already been elaborated")
while len(self._ctrl_stack) > self.domain._depth: while len(self._ctrl_stack) > self.domain._depth:
self._pop_ctrl() self._pop_ctrl()
@ -643,6 +649,8 @@ class Module(_ModuleBuilderRoot, Elaboratable):
if not hasattr(submodule, "elaborate"): if not hasattr(submodule, "elaborate"):
raise TypeError("Trying to add {!r}, which does not implement .elaborate(), as " raise TypeError("Trying to add {!r}, which does not implement .elaborate(), as "
"a submodule".format(submodule)) "a submodule".format(submodule))
if self._frozen:
raise AlreadyElaborated(f"Cannot modify a module that has already been elaborated")
if name == None: if name == None:
self._anon_submodules.append((submodule, src_loc)) self._anon_submodules.append((submodule, src_loc))
else: else:
@ -660,6 +668,8 @@ class Module(_ModuleBuilderRoot, Elaboratable):
def _add_domain(self, cd): def _add_domain(self, cd):
if cd.name in self._domains: if cd.name in self._domains:
raise NameError(f"Clock domain named '{cd.name}' already exists") raise NameError(f"Clock domain named '{cd.name}' already exists")
if self._frozen:
raise AlreadyElaborated(f"Cannot modify a module that has already been elaborated")
self._domains[cd.name] = cd self._domains[cd.name] = cd
def _flush(self): def _flush(self):
@ -668,6 +678,7 @@ class Module(_ModuleBuilderRoot, Elaboratable):
def elaborate(self, platform): def elaborate(self, platform):
self._flush() self._flush()
self._frozen = True
fragment = Fragment(src_loc=self._src_loc) fragment = Fragment(src_loc=self._src_loc)
for name, (submodule, src_loc) in self._named_submodules.items(): for name, (submodule, src_loc) in self._named_submodules.items():

View file

@ -3,17 +3,23 @@ from collections import defaultdict, OrderedDict
import enum import enum
import warnings import warnings
from .._utils import flatten, to_binary from .._utils import flatten, to_binary, final
from .. import tracer, _unused from .. import tracer, _unused
from . import _ast, _cd, _ir, _nir from . import _ast, _cd, _ir, _nir
__all__ = [ __all__ = [
"UnusedElaboratable", "Elaboratable", "DuplicateElaboratable", "DriverConflict", "Fragment", "AlreadyElaborated", "UnusedElaboratable", "Elaboratable", "DuplicateElaboratable",
"Instance", "IOBufferInstance", "PortDirection", "Design", "build_netlist", "DriverConflict", "Fragment", "Instance", "IOBufferInstance", "PortDirection", "Design",
"build_netlist",
] ]
@final
class AlreadyElaborated(Exception):
"""Exception raised when an elaboratable is being modified after elaboration."""
class UnusedElaboratable(_unused.UnusedMustUse): class UnusedElaboratable(_unused.UnusedMustUse):
# The warning is initially silenced. If everything that has been constructed remains unused, # The warning is initially silenced. If everything that has been constructed remains unused,
# it means the application likely crashed (with an exception, or in another way that does not # it means the application likely crashed (with an exception, or in another way that does not

View file

@ -4,17 +4,12 @@ from collections.abc import MutableSequence
from .. import tracer from .. import tracer
from ._ast import * from ._ast import *
from ._ir import Elaboratable, Fragment from ._ir import Elaboratable, Fragment, AlreadyElaborated
from ..utils import ceil_log2 from ..utils import ceil_log2
from .._utils import deprecated, final from .._utils import deprecated, final
__all__ = ["FrozenMemory", "MemoryData", "Memory", "ReadPort", "WritePort", "DummyPort"] __all__ = ["MemoryData", "Memory", "ReadPort", "WritePort", "DummyPort"]
@final
class FrozenMemory(Exception):
"""Exception raised when a memory array is being modified after elaboration."""
@final @final
@ -30,7 +25,7 @@ class MemoryData:
a default value for rows that are not explicitly initialized. a default value for rows that are not explicitly initialized.
Changing the initial contents of a :class:`MemoryData` is only possible until it is used to Changing the initial contents of a :class:`MemoryData` is only possible until it is used to
elaborate a memory; afterwards, attempting to do so will raise :exc:`FrozenMemory`. elaborate a memory; afterwards, attempting to do so will raise :exc:`AlreadyElaborated`.
.. warning:: .. warning::
@ -101,7 +96,7 @@ class MemoryData:
def __setitem__(self, index, value): def __setitem__(self, index, value):
if self._frozen: if self._frozen:
raise FrozenMemory("Cannot set 'init' on a memory that has already been elaborated") raise AlreadyElaborated("Cannot set 'init' on a memory that has already been elaborated")
if isinstance(index, slice): if isinstance(index, slice):
indices = range(*index.indices(len(self._elems))) indices = range(*index.indices(len(self._elems)))
@ -181,7 +176,7 @@ class MemoryData:
@init.setter @init.setter
def init(self, init): def init(self, init):
if self._frozen: if self._frozen:
raise FrozenMemory("Cannot set 'init' on a memory that has already been elaborated") raise AlreadyElaborated("Cannot set 'init' on a memory that has already been elaborated")
self._init = MemoryData.Init(init, shape=self._shape, depth=self._depth) self._init = MemoryData.Init(init, shape=self._shape, depth=self._depth)
def __repr__(self): def __repr__(self):

View file

@ -2,15 +2,14 @@ import operator
from collections import OrderedDict from collections import OrderedDict
from collections.abc import MutableSequence from collections.abc import MutableSequence
from ..hdl import MemoryData, MemoryInstance, Shape, ShapeCastable, Const from ..hdl import MemoryData, MemoryInstance, Shape, ShapeCastable, Const, AlreadyElaborated
from ..hdl._mem import FrozenMemory
from ..utils import ceil_log2 from ..utils import ceil_log2
from .._utils import final from .._utils import final
from .. import tracer from .. import tracer
from . import wiring, data from . import wiring, data
__all__ = ["FrozenMemory", "Memory", "ReadPort", "WritePort"] __all__ = ["Memory", "ReadPort", "WritePort"]
class Memory(wiring.Component): class Memory(wiring.Component):
@ -24,7 +23,7 @@ class Memory(wiring.Component):
the :ref:`elaborate <lang-elaboration>` method. the :ref:`elaborate <lang-elaboration>` method.
Adding ports or changing initial contents of a :class:`Memory` is only possible until it is Adding ports or changing initial contents of a :class:`Memory` is only possible until it is
elaborated; afterwards, attempting to do so will raise :class:`~amaranth.hdl.FrozenMemory`. elaborated; afterwards, attempting to do so will raise :class:`~amaranth.hdl.AlreadyElaborated`.
Platform overrides Platform overrides
------------------ ------------------
@ -121,7 +120,7 @@ class Memory(wiring.Component):
:class:`ReadPort` :class:`ReadPort`
""" """
if self._frozen: if self._frozen:
raise FrozenMemory("Cannot add a memory port to a memory that has already been elaborated") raise AlreadyElaborated("Cannot add a memory port to a memory that has already been elaborated")
signature = ReadPort.Signature(shape=self.shape, addr_width=ceil_log2(self.depth)) signature = ReadPort.Signature(shape=self.shape, addr_width=ceil_log2(self.depth))
return ReadPort(signature, memory=self, domain=domain, transparent_for=transparent_for, return ReadPort(signature, memory=self, domain=domain, transparent_for=transparent_for,
src_loc_at=1 + src_loc_at) src_loc_at=1 + src_loc_at)
@ -146,7 +145,7 @@ class Memory(wiring.Component):
:class:`WritePort` :class:`WritePort`
""" """
if self._frozen: if self._frozen:
raise FrozenMemory("Cannot add a memory port to a memory that has already been elaborated") raise AlreadyElaborated("Cannot add a memory port to a memory that has already been elaborated")
signature = WritePort.Signature( signature = WritePort.Signature(
shape=self.shape, addr_width=ceil_log2(self.depth), granularity=granularity) shape=self.shape, addr_width=ceil_log2(self.depth), granularity=granularity)
return WritePort(signature, memory=self, domain=domain, return WritePort(signature, memory=self, domain=domain,

View file

@ -181,7 +181,8 @@ Simulation
Memory description Memory description
================== ==================
.. autoexception:: amaranth.hdl.FrozenMemory .. autoexception:: amaranth.hdl.AlreadyElaborated
:noindex:
.. autoclass:: amaranth.hdl.MemoryData .. autoclass:: amaranth.hdl.MemoryData

View file

@ -6,6 +6,7 @@ from collections import OrderedDict
from amaranth.hdl._ast import * from amaranth.hdl._ast import *
from amaranth.hdl._cd import * from amaranth.hdl._cd import *
from amaranth.hdl._dsl import * from amaranth.hdl._dsl import *
from amaranth.hdl._ir import *
from amaranth.lib.enum import Enum from amaranth.lib.enum import Enum
from .utils import * from .utils import *
@ -975,3 +976,35 @@ class DSLTestCase(FHDLTestCase):
r"^Domain name should not be prefixed with 'cd_' in `m.domains`, " r"^Domain name should not be prefixed with 'cd_' in `m.domains`, "
r"use `m.domains.rx = ...` instead$"): r"use `m.domains.rx = ...` instead$"):
m.domains.cd_rx = ClockDomain() m.domains.cd_rx = ClockDomain()
def test_freeze(self):
a = Signal()
m = Module()
f = Fragment.get(m, None)
with self.assertRaisesRegex(AlreadyElaborated,
r"^Cannot modify a module that has already been elaborated$"):
m.d.comb += a.eq(1)
with self.assertRaisesRegex(AlreadyElaborated,
r"^Cannot modify a module that has already been elaborated$"):
with m.If(a):
pass
with self.assertRaisesRegex(AlreadyElaborated,
r"^Cannot modify a module that has already been elaborated$"):
with m.Switch(a):
pass
with self.assertRaisesRegex(AlreadyElaborated,
r"^Cannot modify a module that has already been elaborated$"):
with m.FSM():
pass
with self.assertRaisesRegex(AlreadyElaborated,
r"^Cannot modify a module that has already been elaborated$"):
m.submodules.a = Module()
with self.assertRaisesRegex(AlreadyElaborated,
r"^Cannot modify a module that has already been elaborated$"):
m.domains.sync = ClockDomain()

View file

@ -439,15 +439,15 @@ class MemoryTestCase(FHDLTestCase):
m = memory.Memory(shape=unsigned(8), depth=4, init=[]) m = memory.Memory(shape=unsigned(8), depth=4, init=[])
m.write_port() m.write_port()
m.elaborate(None) m.elaborate(None)
with self.assertRaisesRegex(memory.FrozenMemory, with self.assertRaisesRegex(AlreadyElaborated,
r"^Cannot add a memory port to a memory that has already been elaborated$"): r"^Cannot add a memory port to a memory that has already been elaborated$"):
m.write_port() m.write_port()
with self.assertRaisesRegex(memory.FrozenMemory, with self.assertRaisesRegex(AlreadyElaborated,
r"^Cannot add a memory port to a memory that has already been elaborated$"): r"^Cannot add a memory port to a memory that has already been elaborated$"):
m.read_port() m.read_port()
with self.assertRaisesRegex(memory.FrozenMemory, with self.assertRaisesRegex(AlreadyElaborated,
r"^Cannot set 'init' on a memory that has already been elaborated$"): r"^Cannot set 'init' on a memory that has already been elaborated$"):
m.init = [1, 2, 3, 4] m.init = [1, 2, 3, 4]
with self.assertRaisesRegex(memory.FrozenMemory, with self.assertRaisesRegex(AlreadyElaborated,
r"^Cannot set 'init' on a memory that has already been elaborated$"): r"^Cannot set 'init' on a memory that has already been elaborated$"):
m.init[0] = 1 m.init[0] = 1