diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 5ff9a6443..a0bbdd0ad 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -62,11 +62,11 @@ jobs: strategy: fail-fast: false matrix: - project: - - amaranth-lang/amaranth-boards - - amaranth-lang/amaranth-stdio - - amaranth-lang/amaranth-soc - name: 'smoke (${{ matrix.project }})' + project: # test the last commit before dropping py3.8 + - { name: amaranth-lang/amaranth-boards, ref: 19b97324ecf9111c5d16377af79f82aad761c476 } + - { name: amaranth-lang/amaranth-stdio, ref: 2da45b8e75421879d1096495a4fb438de705f567 } + - { name: amaranth-lang/amaranth-soc, ref: 746709e1e992bccf6e2362450243cafd00d72a14 } + name: 'smoke (${{ matrix.project.name }})' steps: - name: Check out Amaranth source code uses: actions/checkout@v4 @@ -76,7 +76,8 @@ jobs: - name: Check out source code uses: actions/checkout@v4 with: - repository: ${{ matrix.project }} + repository: ${{ matrix.project.name }} + ref: ${{ matrix.project.ref }} path: project fetch-depth: 0 - name: Set up PDM diff --git a/amaranth/_unused.py b/amaranth/_unused.py index 6e053435a..4f2507bc1 100644 --- a/amaranth/_unused.py +++ b/amaranth/_unused.py @@ -32,7 +32,7 @@ def __del__(self): return if hasattr(self, "_MustUse__used") and not self._MustUse__used: if get_linter_option(self._MustUse__context["filename"], - self._MustUse__warning.__name__, bool, True): + self._MustUse__warning.__qualname__, bool, True): warnings.warn_explicit( f"{self!r} created but never used", self._MustUse__warning, **self._MustUse__context) diff --git a/amaranth/_utils.py b/amaranth/_utils.py index 938461527..10b125b16 100644 --- a/amaranth/_utils.py +++ b/amaranth/_utils.py @@ -44,7 +44,7 @@ def union(i, start=None): def final(cls): def init_subclass(): raise TypeError("Subclassing {}.{} is not supported" - .format(cls.__module__, cls.__name__)) + .format(cls.__module__, cls.__qualname__)) cls.__init_subclass__ = init_subclass return cls diff --git a/amaranth/back/cxxrtl.py b/amaranth/back/cxxrtl.py index 0fa657b0a..725bda48a 100644 --- a/amaranth/back/cxxrtl.py +++ b/amaranth/back/cxxrtl.py @@ -23,8 +23,8 @@ def _convert_rtlil_text(rtlil_text, black_boxes, *, src_loc_at=0): script = [] if black_boxes is not None: for box_name, box_source in black_boxes.items(): - script.append(f"read_ilang <<rtlil\n{box_source}\nrtlil") - script.append(f"read_ilang <<rtlil\n{rtlil_text}\nrtlil") + script.append(f"read_rtlil <<rtlil\n{box_source}\nrtlil") + script.append(f"read_rtlil <<rtlil\n{rtlil_text}\nrtlil") script.append("write_cxxrtl") return yosys.run(["-q", "-"], "\n".join(script), src_loc_at=1 + src_loc_at) diff --git a/amaranth/back/rtlil.py b/amaranth/back/rtlil.py index cecb11f0f..28f428c1a 100644 --- a/amaranth/back/rtlil.py +++ b/amaranth/back/rtlil.py @@ -1132,7 +1132,7 @@ def emit_print(self, cell_idx, cell): if cell.format is not None: for chunk in cell.format.chunks: if isinstance(chunk, str): - format.append(chunk) + format.append(chunk.replace("{", "{{").replace("}", "}}")) else: spec = _ast.Format._parse_format_spec(chunk.format_desc, _ast.Shape(len(chunk.value), chunk.signed)) type = spec["type"] diff --git a/amaranth/back/verilog.py b/amaranth/back/verilog.py index 6aba92982..5734df62d 100644 --- a/amaranth/back/verilog.py +++ b/amaranth/back/verilog.py @@ -12,7 +12,7 @@ def _convert_rtlil_text(rtlil_text, *, strip_internal_attrs=False, write_verilog yosys = find_yosys(lambda ver: ver >= (0, 40)) script = [] - script.append(f"read_ilang <<rtlil\n{rtlil_text}\nrtlil") + script.append(f"read_rtlil <<rtlil\n{rtlil_text}\nrtlil") script.append("proc -nomux -norom") script.append("memory_collect") diff --git a/amaranth/build/plat.py b/amaranth/build/plat.py index 432ae6770..02dbbaa3b 100644 --- a/amaranth/build/plat.py +++ b/amaranth/build/plat.py @@ -40,7 +40,7 @@ def __init__(self): def default_clk_constraint(self): if self.default_clk is None: raise AttributeError("Platform '{}' does not define a default clock" - .format(type(self).__name__)) + .format(type(self).__qualname__)) return self.lookup(self.default_clk).clock @property @@ -48,7 +48,7 @@ def default_clk_frequency(self): constraint = self.default_clk_constraint if constraint is None: raise AttributeError("Platform '{}' does not constrain its default clock" - .format(type(self).__name__)) + .format(type(self).__qualname__)) return constraint.frequency def add_file(self, filename, content): @@ -120,16 +120,22 @@ def create_missing_domain(self, name): # Many device families provide advanced primitives for tackling reset. If these exist, # they should be used instead. if name == "sync" and self.default_clk is not None: - clk_i = self.request(self.default_clk).i + m = Module() + + clk_io = self.request(self.default_clk, dir="-") + m.submodules.clk_buf = clk_buf = io.Buffer("i", clk_io) + if self.default_rst is not None: - rst_i = self.request(self.default_rst).i + rst_io = self.request(self.default_rst, dir="-") + m.submodules.rst_buf = rst_buf = io.Buffer("i", rst_io) + rst_i = rst_buf.i else: rst_i = Const(0) - m = Module() m.domains += ClockDomain("sync") - m.d.comb += ClockSignal("sync").eq(clk_i) + m.d.comb += ClockSignal("sync").eq(clk_buf.i) m.submodules.reset_sync = ResetSynchronizer(rst_i, domain="sync") + return m def prepare(self, elaboratable, name="top", **kwargs): @@ -173,7 +179,7 @@ def toolchain_program(self, products, name, **kwargs): Extract bitstream for fragment ``name`` from ``products`` and download it to a target. """ raise NotImplementedError("Platform '{}' does not support programming" - .format(type(self).__name__)) + .format(type(self).__qualname__)) class TemplatedPlatform(Platform): @@ -198,14 +204,14 @@ class TemplatedPlatform(Platform): """, } - def iter_clock_constraints(self): - for net_signal, port_signal, frequency in super().iter_clock_constraints(): + def iter_signal_clock_constraints(self): + for signal, frequency in super().iter_signal_clock_constraints(): # Skip any clock constraints placed on signals that are never used in the design. # Otherwise, it will cause a crash in the vendor platform if it supports clock # constraints on non-port nets. - if net_signal not in self._name_map: + if signal not in self._name_map: continue - yield net_signal, port_signal, frequency + yield signal, frequency def toolchain_prepare(self, fragment, name, *, emit_src=True, **kwargs): # Restrict the name of the design to a strict alphanumeric character set. Platforms will @@ -244,7 +250,7 @@ def _extract_override(var, *, expected_type): if issubclass(expected_type, str) and not isinstance(kwarg, str) and isinstance(kwarg, Iterable): kwarg = " ".join(kwarg) if not isinstance(kwarg, expected_type) and not expected_type is None: - raise TypeError(f"Override '{var}' must be a {expected_type.__name__}, not {kwarg!r}") + raise TypeError(f"Override '{var}' must be a {expected_type.__qualname__}, not {kwarg!r}") return kwarg else: return jinja2.Undefined(name=var) @@ -327,8 +333,11 @@ def options(opts): else: return " ".join(opts) - def hierarchy(signal, separator): - return separator.join(self._name_map[signal][1:]) + def hierarchy(net, separator): + if isinstance(net, IOPort): + return net.name + else: + return separator.join(self._name_map[net][1:]) def ascii_escape(string): def escape_one(match): diff --git a/amaranth/build/res.py b/amaranth/build/res.py index 0744ef002..effe44e14 100644 --- a/amaranth/build/res.py +++ b/amaranth/build/res.py @@ -109,8 +109,9 @@ def __init__(self, resources, connectors): # List of (pin, port, buffer) pairs for non-dir="-" requests. self._pins = [] - # Constraint list + # Constraint lists self._clocks = SignalDict() + self._io_clocks = {} self.add_resources(resources) self.add_connectors(connectors) @@ -151,7 +152,8 @@ def request(self, name, number=0, *, dir=None, xdr=None): def merge_options(subsignal, dir, xdr): if isinstance(subsignal.ios[0], Subsignal): - if dir is None: + orig_dir = dir + if dir is None or dir == "-": dir = dict() if xdr is None: xdr = dict() @@ -164,7 +166,7 @@ def merge_options(subsignal, dir, xdr): "has subsignals" .format(xdr, subsignal)) for sub in subsignal.ios: - sub_dir = dir.get(sub.name, None) + sub_dir = "-" if orig_dir == "-" else dir.get(sub.name, None) sub_xdr = xdr.get(sub.name, None) dir[sub.name], xdr[sub.name] = merge_options(sub, sub_dir, sub_xdr) else: @@ -219,6 +221,8 @@ def resolve(resource, dir, xdr, path, attrs): for name in phys_names ]) port = io.SingleEndedPort(iop, invert=phys.invert, direction=direction) + if resource.clock is not None: + self.add_clock_constraint(iop, resource.clock.frequency) if isinstance(phys, DiffPairs): phys_names_p = phys.p.map_names(self._conn_pins, resource) phys_names_n = phys.n.map_names(self._conn_pins, resource) @@ -232,7 +236,8 @@ def resolve(resource, dir, xdr, path, attrs): for name in phys_names_n ]) port = io.DifferentialPort(p, n, invert=phys.invert, direction=direction) - + if resource.clock is not None: + self.add_clock_constraint(p, resource.clock.frequency) for phys_name in phys_names: if phys_name in self._phys_reqd: raise ResourceError("Resource component {} uses physical pin {}, but it " @@ -253,8 +258,6 @@ def resolve(resource, dir, xdr, path, attrs): buffer = PinBuffer(pin, port) self._pins.append((pin, port, buffer)) - if resource.clock is not None: - self.add_clock_constraint(pin.i, resource.clock.frequency) return pin else: @@ -275,38 +278,28 @@ def add_clock_constraint(self, clock, frequency): raise TypeError(f"A clock constraint can only be applied to a Signal, but a " f"ClockSignal is provided; assign the ClockSignal to an " f"intermediate signal and constrain the latter instead.") - elif not isinstance(clock, Signal): - raise TypeError(f"Object {clock!r} is not a Signal") + elif not isinstance(clock, (Signal, IOPort)): + raise TypeError(f"Object {clock!r} is not a Signal or IOPort") if not isinstance(frequency, (int, float)): raise TypeError(f"Frequency must be a number, not {frequency!r}") - if clock in self._clocks: + if isinstance(clock, IOPort): + clocks = self._io_clocks + else: + clocks = self._clocks + + frequency = float(frequency) + if clock in clocks and clocks[clock] != frequency: raise ValueError("Cannot add clock constraint on {!r}, which is already constrained " "to {} Hz" - .format(clock, self._clocks[clock])) + .format(clock, clocks[clock])) else: - self._clocks[clock] = float(frequency) - - def iter_clock_constraints(self): - # Back-propagate constraints through the input buffer. For clock constraints on pins - # (the majority of cases), toolchains work better if the constraint is defined on the pin - # and not on the buffered internal net; and if the toolchain is advanced enough that - # it considers clock phase and delay of the input buffer, it is *necessary* to define - # the constraint on the pin to match the designer's expectation of phase being referenced - # to the pin. - # - # Constraints on nets with no corresponding input pin (e.g. PLL or SERDES outputs) are not - # affected. - pin_i_to_port = SignalDict() - for pin, port, _fragment in self._pins: - if hasattr(pin, "i"): - if isinstance(port, io.SingleEndedPort): - pin_i_to_port[pin.i] = port.io - elif isinstance(port, io.DifferentialPort): - pin_i_to_port[pin.i] = port.p - else: - assert False + clocks[clock] = frequency + + def iter_signal_clock_constraints(self): + for signal, frequency in self._clocks.items(): + yield signal, frequency - for net_signal, frequency in self._clocks.items(): - port_signal = pin_i_to_port.get(net_signal) - yield net_signal, port_signal, frequency + def iter_port_clock_constraints(self): + for port, frequency in self._io_clocks.items(): + yield port, frequency diff --git a/amaranth/hdl/_ast.py b/amaranth/hdl/_ast.py index 8eea92782..e523a95c4 100644 --- a/amaranth/hdl/_ast.py +++ b/amaranth/hdl/_ast.py @@ -244,16 +244,16 @@ def __init__(self, *args, **kwargs): def __init_subclass__(cls, **kwargs): if cls.as_shape is ShapeCastable.as_shape: - raise TypeError(f"Class '{cls.__name__}' deriving from 'ShapeCastable' must override " + raise TypeError(f"Class '{cls.__qualname__}' deriving from 'ShapeCastable' must override " f"the 'as_shape' method") if cls.const is ShapeCastable.const: - raise TypeError(f"Class '{cls.__name__}' deriving from 'ShapeCastable' must override " + raise TypeError(f"Class '{cls.__qualname__}' deriving from 'ShapeCastable' must override " f"the 'const' method") if cls.__call__ is ShapeCastable.__call__: - raise TypeError(f"Class '{cls.__name__}' deriving from 'ShapeCastable' must override " + raise TypeError(f"Class '{cls.__qualname__}' deriving from 'ShapeCastable' must override " f"the '__call__' method") if cls.from_bits is ShapeCastable.from_bits: - warnings.warn(f"Class '{cls.__name__}' deriving from 'ShapeCastable' does not override " + warnings.warn(f"Class '{cls.__qualname__}' deriving from 'ShapeCastable' does not override " f"the 'from_bits' method, which will be required in Amaranth 0.6", DeprecationWarning, stacklevel=2) @@ -1404,10 +1404,10 @@ def __init__(self, *args, **kwargs): def __init_subclass__(cls, **kwargs): if cls.as_value is ValueCastable.as_value: - raise TypeError(f"Class '{cls.__name__}' deriving from 'ValueCastable' must override " + raise TypeError(f"Class '{cls.__qualname__}' deriving from 'ValueCastable' must override " "the 'as_value' method") if cls.shape is ValueCastable.shape: - raise TypeError(f"Class '{cls.__name__}' deriving from 'ValueCastable' must override " + raise TypeError(f"Class '{cls.__qualname__}' deriving from 'ValueCastable' must override " "the 'shape' method") # The signatures and definitions of these methods are weird because they are present here for @@ -1598,6 +1598,10 @@ def cast(obj): def __init__(self, value, shape=None, *, src_loc_at=0): # We deliberately do not call Value.__init__ here. + if isinstance(value, Enum): + if shape is None: + shape = Shape.cast(type(value)) + value = value.value value = int(operator.index(value)) if shape is None: shape = Shape(bits_for(value), signed=value < 0) @@ -1972,6 +1976,57 @@ def __call__(cls, shape=None, src_loc_at=0, **kwargs): return signal +# also used for MemoryData.Init +def _get_init_value(init, shape, what="signal"): + orig_init = init + orig_shape = shape + shape = Shape.cast(shape) + if isinstance(orig_shape, ShapeCastable): + try: + init = Const.cast(orig_shape.const(init)) + except Exception: + raise TypeError(f"Initial value must be a constant initializer of {orig_shape!r}") + if init.shape() != Shape.cast(shape): + raise ValueError(f"Constant returned by {orig_shape!r}.const() must have the shape " + f"that it casts to, {shape!r}, and not {init.shape()!r}") + return init.value + else: + if init is None: + init = 0 + try: + init = Const.cast(init) + except TypeError: + raise TypeError("Initial value must be a constant-castable expression, not {!r}" + .format(orig_init)) + # Avoid false positives for all-zeroes and all-ones + if orig_init is not None and not (isinstance(orig_init, int) and orig_init in (0, -1)): + if init.shape().signed and not shape.signed: + warnings.warn( + message=f"Initial value {orig_init!r} is signed, " + f"but the {what} shape is {shape!r}", + category=SyntaxWarning, + stacklevel=2) + elif (init.shape().width > shape.width or + init.shape().width == shape.width and + shape.signed and not init.shape().signed): + warnings.warn( + message=f"Initial value {orig_init!r} will be truncated to " + f"the {what} shape {shape!r}", + category=SyntaxWarning, + stacklevel=2) + + if isinstance(orig_shape, range) and orig_init is not None and orig_init not in orig_shape: + if orig_init == orig_shape.stop: + raise SyntaxError( + f"Initial value {orig_init!r} equals the non-inclusive end of the {what} " + f"shape {orig_shape!r}; this is likely an off-by-one error") + else: + raise SyntaxError( + f"Initial value {orig_init!r} is not within the {what} shape {orig_shape!r}") + + return Const(init.value, shape).value + + @final class Signal(Value, DUID, metaclass=_SignalMeta): """A varying integer value. @@ -2042,65 +2097,20 @@ def __init__(self, shape=None, *, name=None, init=None, reset=None, reset_less=F DeprecationWarning, stacklevel=2) init = reset - orig_init = init - if isinstance(orig_shape, ShapeCastable): - try: - init = Const.cast(orig_shape.const(init)) - except Exception: - raise TypeError("Initial value must be a constant initializer of {!r}" - .format(orig_shape)) - if init.shape() != Shape.cast(orig_shape): - raise ValueError("Constant returned by {!r}.const() must have the shape that " - "it casts to, {!r}, and not {!r}" - .format(orig_shape, Shape.cast(orig_shape), - init.shape())) - else: - if init is None: - init = 0 - try: - init = Const.cast(init) - except TypeError: - raise TypeError("Initial value must be a constant-castable expression, not {!r}" - .format(orig_init)) - # Avoid false positives for all-zeroes and all-ones - if orig_init is not None and not (isinstance(orig_init, int) and orig_init in (0, -1)): - if init.shape().signed and not self._signed: - warnings.warn( - message="Initial value {!r} is signed, but the signal shape is {!r}" - .format(orig_init, shape), - category=SyntaxWarning, - stacklevel=2) - elif (init.shape().width > self._width or - init.shape().width == self._width and - self._signed and not init.shape().signed): - warnings.warn( - message="Initial value {!r} will be truncated to the signal shape {!r}" - .format(orig_init, shape), - category=SyntaxWarning, - stacklevel=2) - self._init = init.value + self._init = _get_init_value(init, unsigned(1) if orig_shape is None else orig_shape) self._reset_less = bool(reset_less) - if isinstance(orig_shape, range) and orig_init is not None and orig_init not in orig_shape: - if orig_init == orig_shape.stop: - raise SyntaxError( - f"Initial value {orig_init!r} equals the non-inclusive end of the signal " - f"shape {orig_shape!r}; this is likely an off-by-one error") - else: - raise SyntaxError( - f"Initial value {orig_init!r} is not within the signal shape {orig_shape!r}") - self._attrs = OrderedDict(() if attrs is None else attrs) if isinstance(orig_shape, ShapeCastable): self._format = orig_shape.format(orig_shape(self), "") elif isinstance(orig_shape, type) and issubclass(orig_shape, Enum): - self._format = Format.Enum(self, orig_shape, name=orig_shape.__name__) + self._format = Format.Enum(self, orig_shape, name=orig_shape.__qualname__) else: self._format = Format("{}", self) if isinstance(decoder, type) and issubclass(decoder, Enum): - self._format = Format.Enum(self, decoder, name=decoder.__name__) + self._format = Format.Enum(self, decoder, name=decoder.__qualname__) self._decoder = decoder @@ -2552,7 +2562,7 @@ def __format__(self, format_desc): @final class Format(_FormatLike): def __init__(self, format, *args, **kwargs): - fmt = string.Formatter() + fmtter = string.Formatter() chunks = [] used_args = set() auto_arg_index = 0 @@ -2573,29 +2583,29 @@ def get_field(field_name): "specification") auto_arg_index = None - obj, arg_used = fmt.get_field(field_name, args, kwargs) + obj, arg_used = fmtter.get_field(field_name, args, kwargs) used_args.add(arg_used) return obj def subformat(sub_string): result = [] - for literal, field_name, format_spec, conversion in fmt.parse(sub_string): + for literal, field_name, format_spec, conversion in fmtter.parse(sub_string): result.append(literal) if field_name is not None: obj = get_field(field_name) - obj = fmt.convert_field(obj, conversion) + obj = fmtter.convert_field(obj, conversion) format_spec = subformat(format_spec) - result.append(fmt.format_field(obj, format_spec)) + result.append(fmtter.format_field(obj, format_spec)) return "".join(result) - for literal, field_name, format_spec, conversion in fmt.parse(format): + for literal, field_name, format_spec, conversion in fmtter.parse(format): chunks.append(literal) if field_name is not None: obj = get_field(field_name) if conversion == "v": obj = Value.cast(obj) else: - obj = fmt.convert_field(obj, conversion) + obj = fmtter.convert_field(obj, conversion) format_spec = subformat(format_spec) if isinstance(obj, Value): # Perform validation. @@ -2617,7 +2627,7 @@ def subformat(sub_string): raise ValueError(f"Format specifiers ({format_spec!r}) cannot be used for 'Format' objects") chunks += obj._as_format()._chunks else: - chunks.append(fmt.format_field(obj, format_spec)) + chunks.append(fmtter.format_field(obj, format_spec)) for i in range(len(args)): if i not in used_args: @@ -3241,7 +3251,7 @@ def __len__(self): def __repr__(self): pairs = [f"({k!r}, {v!r})" for k, v in self.items()] - return "{}.{}([{}])".format(type(self).__module__, type(self).__name__, + return "{}.{}([{}])".format(type(self).__module__, type(self).__qualname__, ", ".join(pairs)) @@ -3273,7 +3283,7 @@ def __len__(self): return len(self._storage) def __repr__(self): - return "{}.{}({})".format(type(self).__module__, type(self).__name__, + return "{}.{}({})".format(type(self).__module__, type(self).__qualname__, ", ".join(repr(x) for x in self)) @@ -3303,7 +3313,7 @@ def __lt__(self, other): return self._intern < other._intern def __repr__(self): - return f"<{__name__}.SignalKey {self.signal!r}>" + return f"<{type(self).__qualname__} {self.signal!r}>" class SignalDict(_MappedKeyDict): diff --git a/amaranth/hdl/_dsl.py b/amaranth/hdl/_dsl.py index c66df2e21..9cf0b2b4b 100644 --- a/amaranth/hdl/_dsl.py +++ b/amaranth/hdl/_dsl.py @@ -19,82 +19,78 @@ __all__ = ["SyntaxError", "SyntaxWarning", "Module"] -class _Visitor: - def __init__(self): - self.driven_signals = SignalSet() - - def visit_stmt(self, stmt): - if isinstance(stmt, _StatementList): - for s in stmt: - self.visit_stmt(s) - elif isinstance(stmt, Assign): - self.visit_lhs(stmt.lhs) - self.visit_rhs(stmt.rhs) - elif isinstance(stmt, Print): +def _check_stmt(stmt): + if isinstance(stmt, _StatementList): + for s in stmt: + _check_stmt(s) + elif isinstance(stmt, Assign): + _check_lhs(stmt.lhs) + _check_rhs(stmt.rhs) + elif isinstance(stmt, Print): + for chunk in stmt.message._chunks: + if not isinstance(chunk, str): + obj, format_spec = chunk + _check_rhs(obj) + elif isinstance(stmt, Property): + _check_rhs(stmt.test) + if stmt.message is not None: for chunk in stmt.message._chunks: if not isinstance(chunk, str): obj, format_spec = chunk - self.visit_rhs(obj) - elif isinstance(stmt, Property): - self.visit_rhs(stmt.test) - if stmt.message is not None: - for chunk in stmt.message._chunks: - if not isinstance(chunk, str): - obj, format_spec = chunk - self.visit_rhs(obj) - elif isinstance(stmt, Switch): - self.visit_rhs(stmt.test) - for _patterns, stmts, _src_loc in stmt.cases: - self.visit_stmt(stmts) - elif isinstance(stmt, _LateBoundStatement): - pass - else: - assert False # :nocov: - - def visit_lhs(self, value): - if isinstance(value, Operator) and value.operator in ("u", "s"): - self.visit_lhs(value.operands[0]) - elif isinstance(value, (Signal, ClockSignal, ResetSignal)): - self.driven_signals.add(value) - elif isinstance(value, Slice): - self.visit_lhs(value.value) - elif isinstance(value, Part): - self.visit_lhs(value.value) - self.visit_rhs(value.offset) - elif isinstance(value, Concat): - for part in value.parts: - self.visit_lhs(part) - elif isinstance(value, SwitchValue): - self.visit_rhs(value.test) - for _patterns, elem in value.cases: - self.visit_lhs(elem) - elif isinstance(value, MemoryData._Row): - raise ValueError(f"Value {value!r} can only be used in simulator processes") - else: - raise ValueError(f"Value {value!r} cannot be assigned to") - - def visit_rhs(self, value): - if isinstance(value, (Const, Signal, ClockSignal, ResetSignal, Initial, AnyValue)): - pass - elif isinstance(value, Operator): - for op in value.operands: - self.visit_rhs(op) - elif isinstance(value, Slice): - self.visit_rhs(value.value) - elif isinstance(value, Part): - self.visit_rhs(value.value) - self.visit_rhs(value.offset) - elif isinstance(value, Concat): - for part in value.parts: - self.visit_rhs(part) - elif isinstance(value, SwitchValue): - self.visit_rhs(value.test) - for _patterns, elem in value.cases: - self.visit_rhs(elem) - elif isinstance(value, MemoryData._Row): - raise ValueError(f"Value {value!r} can only be used in simulator processes") - else: - assert False # :nocov: + _check_rhs(obj) + elif isinstance(stmt, Switch): + _check_rhs(stmt.test) + for _patterns, stmts, _src_loc in stmt.cases: + _check_stmt(stmts) + elif isinstance(stmt, _LateBoundStatement): + pass + else: + assert False # :nocov: + +def _check_lhs(value): + if isinstance(value, Operator) and value.operator in ("u", "s"): + _check_lhs(value.operands[0]) + elif isinstance(value, (Signal, ClockSignal, ResetSignal)): + pass + elif isinstance(value, Slice): + _check_lhs(value.value) + elif isinstance(value, Part): + _check_lhs(value.value) + _check_rhs(value.offset) + elif isinstance(value, Concat): + for part in value.parts: + _check_lhs(part) + elif isinstance(value, SwitchValue): + _check_rhs(value.test) + for _patterns, elem in value.cases: + _check_lhs(elem) + elif isinstance(value, MemoryData._Row): + raise ValueError(f"Value {value!r} can only be used in simulator processes") + else: + raise ValueError(f"Value {value!r} cannot be assigned to") + +def _check_rhs(value): + if isinstance(value, (Const, Signal, ClockSignal, ResetSignal, Initial, AnyValue)): + pass + elif isinstance(value, Operator): + for op in value.operands: + _check_rhs(op) + elif isinstance(value, Slice): + _check_rhs(value.value) + elif isinstance(value, Part): + _check_rhs(value.value) + _check_rhs(value.offset) + elif isinstance(value, Concat): + for part in value.parts: + _check_rhs(part) + elif isinstance(value, SwitchValue): + _check_rhs(value.test) + for _patterns, elem in value.cases: + _check_rhs(elem) + elif isinstance(value, MemoryData._Row): + raise ValueError(f"Value {value!r} can only be used in simulator processes") + else: + assert False # :nocov: class _ModuleBuilderProxy: @@ -144,9 +140,9 @@ def __init__(self, builder, depth): def __getattr__(self, name): if name in ("comb", "sync"): raise AttributeError("'{}' object has no attribute '{}'; did you mean 'd.{}'?" - .format(type(self).__name__, name, name)) + .format(type(self).__qualname__, name, name)) raise AttributeError("'{}' object has no attribute '{}'" - .format(type(self).__name__, name)) + .format(type(self).__qualname__, name)) class _ModuleBuilderSubmodules: @@ -632,16 +628,27 @@ def _add_statement(self, assigns, domain, depth): stmt._MustUse__used = True - visitor = _Visitor() - visitor.visit_stmt(stmt) - for signal in visitor.driven_signals: - if signal not in self._driving: - self._driving[signal] = domain - elif self._driving[signal] != domain: - cd_curr = self._driving[signal] - raise SyntaxError( - f"Driver-driver conflict: trying to drive {signal!r} from d.{domain}, but it is " - f"already driven from d.{cd_curr}") + _check_stmt(stmt) + + lhs_masks = LHSMaskCollector() + # This is an opportunistic early check — not much harm skipping it, since + # the whole-design check will be later done in NIR emitter. + if not isinstance(stmt, _LateBoundStatement): + lhs_masks.visit_stmt(stmt) + + for sig, mask in lhs_masks.masks(): + if sig not in self._driving: + self._driving[sig] = [None] * len(sig) + sig_domain = self._driving[sig] + for bit in range(len(sig)): + if not (mask & (1 << bit)): + continue + if sig_domain[bit] is None: + sig_domain[bit] = domain + if sig_domain[bit] != domain: + raise SyntaxError( + f"Driver-driver conflict: trying to drive {sig!r} bit {bit} from d.{domain}, but it is " + f"already driven from d.{sig_domain[bit]}") self._statements.setdefault(domain, []).append(stmt) diff --git a/amaranth/hdl/_ir.py b/amaranth/hdl/_ir.py index f24cd2e41..90ddb8a48 100644 --- a/amaranth/hdl/_ir.py +++ b/amaranth/hdl/_ir.py @@ -728,17 +728,40 @@ def __init__(self, module_idx: int, signal: _ast.Signal, self.src_loc = src_loc self.assignments = [] - def emit_value(self, builder): + def emit_value(self, builder, chunk_start, chunk_end): + chunk_len = chunk_end - chunk_start if self.domain is None: init = _ast.Const(self.signal.init, len(self.signal)) default, _signed = builder.emit_rhs(self.module_idx, init) else: default = builder.emit_signal(self.signal) - if len(self.assignments) == 1: - assign, = self.assignments - if assign.cond == 1 and assign.start == 0 and len(assign.value) == len(default): - return assign.value - cell = _nir.AssignmentList(self.module_idx, default=default, assignments=self.assignments, + default = default[chunk_start:chunk_end] + assignments = [] + for assign in self.assignments: + if assign.start >= chunk_end: + continue + if assign.start + len(assign.value) <= chunk_start: + continue + if assign.cond == 1 and assign.start == chunk_start and len(assign.value) == chunk_len and len(assignments) == 0: + default = assign.value + else: + if assign.start < chunk_start: + start = 0 + value = assign.value[chunk_start - assign.start:] + else: + start = assign.start - chunk_start + value = assign.value + if start + len(value) > chunk_len: + value = value[:chunk_len - start] + assignments.append(_nir.Assignment( + cond=assign.cond, + start=start, + value=value, + src_loc=assign.src_loc + )) + if len(assignments) == 0: + return default + cell = _nir.AssignmentList(self.module_idx, default=default, assignments=assignments, src_loc=self.signal.src_loc) return builder.netlist.add_value_cell(len(default), cell) @@ -748,6 +771,7 @@ def __init__(self, netlist: _nir.Netlist, design: Design, *, all_undef_to_ff=Fal self.netlist = netlist self.design = design self.all_undef_to_ff = all_undef_to_ff + # SignalDict from Signal to dict from (module index, ClockDomain | None) to NetlistDriver self.drivers = _ast.SignalDict() self.io_ports: dict[_ast.IOPort, int] = {} self.rhs_cache: dict[int, Tuple[_nir.Value, bool, _ast.Value]] = {} @@ -1064,24 +1088,13 @@ def connect(self, lhs: _nir.Value, rhs: _nir.Value, *, src_loc): def emit_assign(self, module_idx: int, cd: "_cd.ClockDomain | None", lhs: _ast.Value, lhs_start: int, rhs: _nir.Value, cond: _nir.Net, *, src_loc): # Assign rhs to lhs[lhs_start:lhs_start+len(rhs)] if isinstance(lhs, _ast.Signal): - if lhs in self.drivers: - driver = self.drivers[lhs] - if driver.domain is not cd: - domain_name = cd.name if cd is not None else "comb" - other_domain_name = driver.domain.name if driver.domain is not None else "comb" - raise _ir.DriverConflict( - f"Signal {lhs!r} driven from domain {domain_name} at {src_loc[0]}:{src_loc[1]} and domain " - f"{other_domain_name} at {driver.src_loc[0]}:{driver.src_loc[1]}") - if driver.module_idx != module_idx: - mod_name = ".".join(self.netlist.modules[module_idx].name or ("<toplevel>",)) - other_mod_name = \ - ".".join(self.netlist.modules[driver.module_idx].name or ("<toplevel>",)) - raise _ir.DriverConflict( - f"Signal {lhs!r} driven from module {mod_name} at {src_loc[0]}:{src_loc[1]} and " - f"module {other_mod_name} at {driver.src_loc[0]}:{driver.src_loc[1]}") + sig_drivers = self.drivers.setdefault(lhs, {}) + key = (module_idx, cd) + if key in sig_drivers: + driver = sig_drivers[key] else: driver = NetlistDriver(module_idx, lhs, domain=cd, src_loc=src_loc) - self.drivers[lhs] = driver + sig_drivers[key] = driver driver.assignments.append(_nir.Assignment(cond=cond, start=lhs_start, value=rhs, src_loc=src_loc)) elif isinstance(lhs, _ast.Slice): @@ -1425,43 +1438,104 @@ def emit_format(path, fmt): self.netlist.signal_fields[signal] = fields def emit_drivers(self): - for driver in self.drivers.values(): - if (driver.domain is not None and - driver.domain.rst is not None and - not driver.domain.async_reset and - not driver.signal.reset_less): - cond = self.emit_matches(driver.module_idx, - self.emit_signal(driver.domain.rst), - ("1",), - src_loc=driver.domain.rst.src_loc) - cond, = self.emit_priority_match(driver.module_idx, _nir.Net.from_const(1), - _nir.Value(cond), - src_loc=driver.domain.rst.src_loc) - init = _nir.Value.from_const(driver.signal.init, len(driver.signal)) - driver.assignments.append(_nir.Assignment(cond=cond, start=0, - value=init, src_loc=driver.signal.src_loc)) - value = driver.emit_value(self) - if driver.domain is not None: - clk, = self.emit_signal(driver.domain.clk) - if driver.domain.rst is not None and driver.domain.async_reset and not driver.signal.reset_less: - arst, = self.emit_signal(driver.domain.rst) + for sig, sig_drivers in self.drivers.items(): + driven_bits = [None] * len(sig) + for driver in sig_drivers.values(): + lhs = self.emit_signal(driver.signal) + if len(sig_drivers) == 1 and all(net not in self.netlist.connections for net in lhs): + # If the signal is only assigned from one (module, clock domain) pair, and is + # also not driven by any instance, extend this driver to cover all bits of + # the signal for nicer netlist output. + driver_mask = (1 << len(sig)) - 1 + driver_bit_start = 0 + driver_bit_stop = len(sig) else: - arst = _nir.Net.from_const(0) - cell = _nir.FlipFlop(driver.module_idx, - data=value, - init=driver.signal.init, - clk=clk, - clk_edge=driver.domain.clk_edge, - arst=arst, - attributes=driver.signal.attrs, - src_loc=driver.signal.src_loc, - ) - value = self.netlist.add_value_cell(len(value), cell) - if driver.assignments: - src_loc = driver.assignments[0].src_loc - else: - src_loc = driver.signal.src_loc - self.connect(self.emit_signal(driver.signal), value, src_loc=src_loc) + # Otherwise, per-bit assignment it is. + driver_mask = 0 + driver_bit_start = len(sig) + driver_bit_stop = 0 + for assign in driver.assignments: + for bit in range(assign.start, assign.start + len(assign.value)): + driver_mask |= 1 << bit + # The conflict would be caught by connect anyway, but we can have + # a slightly better error message this way (showing the exact colliding + # domains) + if driven_bits[bit] is not None: + other_module_idx, other_domain, other_src_loc = driven_bits[bit] + if other_domain != driver.domain: + domain_name = driver.domain.name if driver.domain is not None else "comb" + other_domain_name = other_domain.name if other_domain is not None else "comb" + raise _ir.DriverConflict( + f"Signal {sig!r} bit {bit} driven from domain {domain_name} at " + f"{assign.src_loc[0]}:{assign.src_loc[1]} and domain " + f"{other_domain_name} at {other_src_loc[0]}:{other_src_loc[1]}") + if other_module_idx != driver.module_idx: + mod_name = ".".join(self.netlist.modules[driver.module_idx].name or ("<toplevel>",)) + other_mod_name = \ + ".".join(self.netlist.modules[other_module_idx].name or ("<toplevel>",)) + raise _ir.DriverConflict( + f"Signal {sig!r} bit {bit} driven from module {mod_name} at " + f"{assign.src_loc[0]}:{assign.src_loc[1]} and " + f"module {other_mod_name} at " + f"{other_src_loc[0]}:{other_src_loc[1]}") + else: + driven_bits[bit] = (driver.module_idx, driver.domain, assign.src_loc) + + driver_chunks = [] + pos = 0 + while pos < len(sig): + if driver_mask & 1 << pos: + end_pos = pos + while driver_mask & 1 << end_pos: + end_pos += 1 + driver_chunks.append((pos, end_pos)) + pos = end_pos + else: + pos += 1 + + + if (driver.domain is not None and + driver.domain.rst is not None and + not driver.domain.async_reset and + not driver.signal.reset_less): + cond = self.emit_matches(driver.module_idx, + self.emit_signal(driver.domain.rst), + ("1",), + src_loc=driver.domain.rst.src_loc) + cond, = self.emit_priority_match(driver.module_idx, _nir.Net.from_const(1), + _nir.Value(cond), + src_loc=driver.domain.rst.src_loc) + init = _nir.Value.from_const(driver.signal.init, len(driver.signal)) + driver.assignments.append(_nir.Assignment(cond=cond, start=0, + value=init, src_loc=driver.signal.src_loc)) + + for chunk_start, chunk_end in driver_chunks: + chunk_len = chunk_end - chunk_start + chunk_mask = (1 << chunk_len) - 1 + + value = driver.emit_value(self, chunk_start, chunk_end) + if driver.domain is not None: + clk, = self.emit_signal(driver.domain.clk) + if driver.domain.rst is not None and driver.domain.async_reset and not driver.signal.reset_less: + arst, = self.emit_signal(driver.domain.rst) + else: + arst = _nir.Net.from_const(0) + cell = _nir.FlipFlop(driver.module_idx, + data=value, + init=(driver.signal.init >> chunk_start) & chunk_mask, + clk=clk, + clk_edge=driver.domain.clk_edge, + arst=arst, + attributes=driver.signal.attrs, + src_loc=driver.signal.src_loc, + ) + value = self.netlist.add_value_cell(len(value), cell) + if driver.assignments: + src_loc = driver.assignments[0].src_loc + else: + src_loc = driver.signal.src_loc + + self.connect(lhs[chunk_start:chunk_end], value, src_loc=src_loc) def emit_undef_ff(self): # Connect all completely undriven signals to flip-flops with const-0 clock. This is used diff --git a/amaranth/hdl/_mem.py b/amaranth/hdl/_mem.py index add1a896f..8a2dd5504 100644 --- a/amaranth/hdl/_mem.py +++ b/amaranth/hdl/_mem.py @@ -4,6 +4,7 @@ from .. import tracer from ._ast import * +from ._ast import _get_init_value from ._ir import Elaboratable, Fragment, AlreadyElaborated from ..utils import ceil_log2 from .._utils import deprecated, final @@ -106,10 +107,11 @@ def __setitem__(self, index, value): for actual_index, actual_value in zip(indices, value): self[actual_index] = actual_value else: + raw = _get_init_value(value, self._shape, "memory") if isinstance(self._shape, ShapeCastable): - self._raw[index] = Const.cast(Const(value, self._shape)).value + self._raw[index] = raw else: - value = operator.index(value) + value = raw # self._raw[index] assigned by the following line self._elems[index] = value diff --git a/amaranth/hdl/_nir.py b/amaranth/hdl/_nir.py index 7f9c26867..c15247a1a 100644 --- a/amaranth/hdl/_nir.py +++ b/amaranth/hdl/_nir.py @@ -465,7 +465,7 @@ def traverse(net): elif isinstance(obj, Operator): obj = f"operator {obj.operator}" else: - obj = f"cell {obj.__class__.__name__}" + obj = f"cell {obj.__class__.__qualname__}" src_loc = "<unknown>:0" if src_loc is None else f"{src_loc[0]}:{src_loc[1]}" msg.append(f" {src_loc}: {obj} bit {bit}\n") raise CombinationalCycle("".join(msg)) @@ -656,7 +656,7 @@ class Operator(Cell): The ternary operators are: - - 'm': multiplexer, first input needs to have width of 1, second and third operand need to have + - 'm': multiplexer, first input needs to have width of 1, second and third operand need to have the same width as output; implements arg0 ? arg1 : arg2 Attributes @@ -921,8 +921,8 @@ def __repr__(self): def comb_edges_to(self, bit): yield (self.default[bit], self.src_loc) for assign in self.assignments: - yield (assign.cond, assign.src_loc) if bit >= assign.start and bit < assign.start + len(assign.value): + yield (assign.cond, assign.src_loc) yield (assign.value[bit - assign.start], assign.src_loc) diff --git a/amaranth/hdl/_xfrm.py b/amaranth/hdl/_xfrm.py index 92d070594..d04bbbad6 100644 --- a/amaranth/hdl/_xfrm.py +++ b/amaranth/hdl/_xfrm.py @@ -17,6 +17,7 @@ "FragmentTransformer", "TransformedElaboratable", "DomainCollector", "DomainRenamer", "DomainLowerer", + "LHSMaskCollector", "ResetInserter", "EnableInserter"] @@ -601,6 +602,71 @@ def on_fragment(self, fragment): return super().on_fragment(fragment) +class LHSMaskCollector: + def __init__(self): + self.lhs = SignalDict() + + def visit_stmt(self, stmt): + if type(stmt) is Assign: + self.visit_value(stmt.lhs, ~0) + elif type(stmt) is Switch: + for (_, substmt, _) in stmt.cases: + self.visit_stmt(substmt) + elif type(stmt) in (Property, Print): + pass + elif isinstance(stmt, Iterable): + for substmt in stmt: + self.visit_stmt(substmt) + else: + assert False # :nocov: + + def visit_value(self, value, mask): + if type(value) in (Signal, ClockSignal, ResetSignal): + mask &= (1 << len(value)) - 1 + self.lhs.setdefault(value, 0) + self.lhs[value] |= mask + elif type(value) is Operator: + assert value.operator in ("s", "u") + self.visit_value(value.operands[0], mask) + elif type(value) is Slice: + slice_mask = (1 << value.stop) - (1 << value.start) + mask <<= value.start + mask &= slice_mask + self.visit_value(value.value, mask) + elif type(value) is Part: + # Could be more accurate, but if you're relying on such details, you're not seeing + # the Light of Heaven. + self.visit_value(value.value, ~0) + elif type(value) is Concat: + for part in value.parts: + self.visit_value(part, mask) + mask >>= len(part) + elif type(value) is SwitchValue: + for (_, subvalue) in value.cases: + self.visit_value(subvalue, mask) + else: + assert False # :nocov: + + def chunks(self): + for signal, mask in self.lhs.items(): + if mask == (1 << len(signal)) - 1: + yield signal, 0, None + else: + start = 0 + while start < len(signal): + if ((mask >> start) & 1) == 0: + start += 1 + else: + stop = start + while stop < len(signal) and ((mask >> stop) & 1) == 1: + stop += 1 + yield (signal, start, stop) + start = stop + + def masks(self): + yield from self.lhs.items() + + class _ControlInserter(FragmentTransformer): def __init__(self, controls): self.src_loc = None @@ -615,10 +681,9 @@ def on_fragment(self, fragment): for domain, statements in fragment.statements.items(): if domain == "comb" or domain not in self.controls: continue - signals = SignalSet() - for stmt in statements: - signals |= stmt._lhs_signals() - self._insert_control(new_fragment, domain, signals) + lhs_masks = LHSMaskCollector() + lhs_masks.visit_stmt(statements) + self._insert_control(new_fragment, domain, lhs_masks) return new_fragment def _insert_control(self, fragment, domain, signals): @@ -630,13 +695,20 @@ def __call__(self, value, *, src_loc_at=0): class ResetInserter(_ControlInserter): - def _insert_control(self, fragment, domain, signals): - stmts = [s.eq(Const(s.init, s.shape())) for s in signals if not s.reset_less] + def _insert_control(self, fragment, domain, lhs_masks): + stmts = [] + for signal, start, stop in lhs_masks.chunks(): + if signal.reset_less: + continue + if start == 0 and stop is None: + stmts.append(signal.eq(Const(signal.init, signal.shape()))) + else: + stmts.append(signal[start:stop].eq(Const(signal.init, signal.shape())[start:stop])) fragment.add_statements(domain, Switch(self.controls[domain], [(1, stmts, None)], src_loc=self.src_loc)) class EnableInserter(_ControlInserter): - def _insert_control(self, fragment, domain, signals): + def _insert_control(self, fragment, domain, _lhs_masks): if domain in fragment.statements: fragment.statements[domain] = _StatementList([Switch( self.controls[domain], diff --git a/amaranth/lib/cdc.py b/amaranth/lib/cdc.py index 1afcc7698..9939c383b 100644 --- a/amaranth/lib/cdc.py +++ b/amaranth/lib/cdc.py @@ -96,7 +96,7 @@ def elaborate(self, platform): if self._max_input_delay is not None: raise NotImplementedError("Platform '{}' does not support constraining input delay " "for FFSynchronizer" - .format(type(platform).__name__)) + .format(type(platform).__qualname__)) m = Module() flops = [Signal(self.i.shape(), name=f"stage{index}", @@ -170,7 +170,7 @@ def elaborate(self, platform): if self._max_input_delay is not None: raise NotImplementedError("Platform '{}' does not support constraining input delay " "for AsyncFFSynchronizer" - .format(type(platform).__name__)) + .format(type(platform).__qualname__)) m = Module() m.domains += ClockDomain("async_ff", async_reset=True, local=True) diff --git a/amaranth/lib/data.py b/amaranth/lib/data.py index 623d3274c..87624f0a4 100644 --- a/amaranth/lib/data.py +++ b/amaranth/lib/data.py @@ -219,12 +219,16 @@ def const(self, init): elif isinstance(init, Sequence): iterator = enumerate(init) else: - raise TypeError("Layout constant initializer must be a mapping or a sequence, not {!r}" - .format(init)) + raise TypeError(f"Layout constant initializer must be a mapping or a sequence, not " + f"{init!r}") int_value = 0 for key, key_value in iterator: - field = self[key] + try: + field = self[key] + except KeyError: + raise ValueError(f"Layout constant initializer refers to key {key!r}, which is not " + f"a part of the layout") cast_field_shape = Shape.cast(field.shape) if isinstance(field.shape, ShapeCastable): key_value = hdl.Const.cast(hdl.Const(key_value, field.shape)) @@ -792,17 +796,36 @@ def __getitem__(self, key): :exc:`TypeError` If :meth:`.ShapeCastable.__call__` does not return a value or a value-castable object. """ - if isinstance(key, slice): - raise TypeError( - "View cannot be indexed with a slice; did you mean to call `.as_value()` first?") if isinstance(self.__layout, ArrayLayout): - if not isinstance(key, (int, Value, ValueCastable)): + elem_width = Shape.cast(self.__layout.elem_shape).width + if isinstance(key, slice): + start, stop, stride = key.indices(self.__layout.length) + shape = ArrayLayout(self.__layout.elem_shape, len(range(start, stop, stride))) + if stride == 1: + value = self.__target[start * elem_width:stop * elem_width] + else: + value = Cat(self.__target[index * elem_width:(index + 1) * elem_width] + for index in range(start, stop, stride)) + elif isinstance(key, int): + if key not in range(-self.__layout.length, self.__layout.length): + raise IndexError(f"Index {key} is out of range for array layout of length " + f"{self.__layout.length}") + if key < 0: + key += self.__layout.length + shape = self.__layout.elem_shape + value = self.__target[key * elem_width:(key + 1) * elem_width] + elif isinstance(key, (Value, ValueCastable)): + shape = self.__layout.elem_shape + value = self.__target.word_select(key, elem_width) + else: raise TypeError( f"View with array layout may only be indexed with an integer or a value, " f"not {key!r}") - shape = self.__layout.elem_shape - value = self.__target.word_select(key, Shape.cast(self.__layout.elem_shape).width) else: + if isinstance(key, slice): + raise TypeError( + "Non-array view cannot be indexed with a slice; did you mean to call " + "`.as_value()` first?") if isinstance(key, (Value, ValueCastable)): raise TypeError( f"Only views with array layout, not {self.__layout!r}, may be indexed with " @@ -850,6 +873,12 @@ def __getattr__(self, name): f"may only be accessed by indexing") return item + def __len__(self): + if not isinstance(self.__layout, ArrayLayout): + raise TypeError( + f"`len()` can only be used on views of array layout, not {self.__layout!r}") + return self.__layout.length + def __eq__(self, other): if isinstance(other, View) and self.__layout == other.__layout: return self.__target == other.__target @@ -901,7 +930,7 @@ def __and__(self, other): __rxor__ = __and__ def __repr__(self): - return f"{self.__class__.__name__}({self.__layout!r}, {self.__target!r})" + return f"{self.__class__.__qualname__}({self.__layout!r}, {self.__target!r})" class Const(ValueCastable): @@ -1015,16 +1044,38 @@ def __getitem__(self, key): :meth:`.ShapeCastable.from_bits`. Usually this will be a :exc:`ValueError`. """ if isinstance(self.__layout, ArrayLayout): - if isinstance(key, (Value, ValueCastable)): + elem_width = Shape.cast(self.__layout.elem_shape).width + if isinstance(key, slice): + start, stop, stride = key.indices(self.__layout.length) + shape = ArrayLayout(self.__layout.elem_shape, len(range(start, stop, stride))) + if stride == 1: + value = (self.__target >> start * elem_width) & ((1 << elem_width * (stop - start)) - 1) + else: + value = 0 + pos = 0 + for index in range(start, stop, stride): + elem_value = (self.__target >> index * elem_width) & ((1 << elem_width) - 1) + value |= elem_value << pos + pos += elem_width + elif isinstance(key, int): + if key not in range(-self.__layout.length, self.__layout.length): + raise IndexError(f"Index {key} is out of range for array layout of length " + f"{self.__layout.length}") + if key < 0: + key += self.__layout.length + shape = self.__layout.elem_shape + value = (self.__target >> key * elem_width) & ((1 << elem_width) - 1) + elif isinstance(key, (Value, ValueCastable)): return View(self.__layout, self.as_value())[key] - if not isinstance(key, int): + else: raise TypeError( f"Constant with array layout may only be indexed with an integer or a value, " f"not {key!r}") - shape = self.__layout.elem_shape - elem_width = Shape.cast(self.__layout.elem_shape).width - value = (self.__target >> key * elem_width) & ((1 << elem_width) - 1) else: + if isinstance(key, slice): + raise TypeError( + "Non-array constant cannot be indexed with a slice; did you mean to call " + "`.as_value()` first?") if isinstance(key, (Value, ValueCastable)): raise TypeError( f"Only constants with array layout, not {self.__layout!r}, may be indexed with " @@ -1067,15 +1118,30 @@ def __getattr__(self, name): f"may only be accessed by indexing") return item + def __len__(self): + if not isinstance(self.__layout, ArrayLayout): + raise TypeError( + f"`len()` can only be used on constants of array layout, not {self.__layout!r}") + return self.__layout.length + def __eq__(self, other): if isinstance(other, View) and self.__layout == other._View__layout: return self.as_value() == other._View__target elif isinstance(other, Const) and self.__layout == other.__layout: return self.__target == other.__target else: + cause = None + if isinstance(other, (dict, list)): + try: + other_as_const = self.__layout.const(other) + except (TypeError, ValueError) as exc: + cause = exc + else: + return self == other_as_const raise TypeError( - f"Constant with layout {self.__layout!r} can only be compared to another view or " - f"constant with the same layout, not {other!r}") + f"Constant with layout {self.__layout!r} can only be compared to another view, " + f"a constant with the same layout, or a dictionary or a list that can be converted " + f"to a constant with the same layout, not {other!r}") from cause def __ne__(self, other): if isinstance(other, View) and self.__layout == other._View__layout: @@ -1083,9 +1149,18 @@ def __ne__(self, other): elif isinstance(other, Const) and self.__layout == other.__layout: return self.__target != other.__target else: + cause = None + if isinstance(other, (dict, list)): + try: + other_as_const = self.__layout.const(other) + except (TypeError, ValueError) as exc: + cause = exc + else: + return self != other_as_const raise TypeError( - f"Constant with layout {self.__layout!r} can only be compared to another view or " - f"constant with the same layout, not {other!r}") + f"Constant with layout {self.__layout!r} can only be compared to another view, " + f"a constant with the same layout, or a dictionary or a list that can be converted " + f"to a constant with the same layout, not {other!r}") from cause def __add__(self, other): raise TypeError("Cannot perform arithmetic operations on a lib.data.Const") @@ -1118,7 +1193,7 @@ def __and__(self, other): __rxor__ = __and__ def __repr__(self): - return f"{self.__class__.__name__}({self.__layout!r}, {self.__target!r})" + return f"{self.__class__.__qualname__}({self.__layout!r}, {self.__target!r})" class _AggregateMeta(ShapeCastable, type): @@ -1230,7 +1305,7 @@ def is_subnormal(self): >>> IEEE754Single.as_shape() StructLayout({'fraction': 23, 'exponent': 8, 'sign': 1}) - >>> Signal(IEEE754Single).as_value().width + >>> Signal(IEEE754Single).as_value().shape().width 32 Instances of this class can be used where :ref:`values <lang-values>` are expected: diff --git a/amaranth/lib/enum.py b/amaranth/lib/enum.py index 032f281a7..d6463c3bd 100644 --- a/amaranth/lib/enum.py +++ b/amaranth/lib/enum.py @@ -178,7 +178,7 @@ def from_bits(cls, bits): def format(cls, value, format_spec): if format_spec != "": raise ValueError(f"Format specifier {format_spec!r} is not supported for enums") - return Format.Enum(value, cls, name=cls.__name__) + return Format.Enum(value, cls, name=cls.__qualname__) # In 3.11, Python renamed EnumMeta to EnumType. Like Python itself, we support both for @@ -310,7 +310,7 @@ def __ne__(self, other): return self.target != other.target def __repr__(self): - return f"{type(self).__name__}({self.enum.__name__}, {self.target!r})" + return f"{type(self).__qualname__}({self.enum.__qualname__}, {self.target!r})" class FlagView(EnumView): diff --git a/amaranth/lib/io.py b/amaranth/lib/io.py index e4ea8b2ec..6eed2bf76 100644 --- a/amaranth/lib/io.py +++ b/amaranth/lib/io.py @@ -1,5 +1,6 @@ import enum import operator +import warnings from abc import ABCMeta, abstractmethod from collections.abc import Iterable @@ -11,7 +12,7 @@ __all__ = [ - "Direction", "PortLike", "SingleEndedPort", "DifferentialPort", + "Direction", "PortLike", "SingleEndedPort", "DifferentialPort", "SimulationPort", "Buffer", "FFBuffer", "DDRBuffer", "Pin", ] @@ -57,6 +58,12 @@ class PortLike(metaclass=ABCMeta): :class:`amaranth.hdl.IOPort` is not an instance of :class:`amaranth.lib.io.PortLike`. """ + # TODO(amaranth-0.6): remove + def __init_subclass__(cls): + if cls.__add__ is PortLike.__add__: + warnings.warn(f"{cls.__module__}.{cls.__qualname__} must override the `__add__` method", + DeprecationWarning, stacklevel=2) + @property @abstractmethod def direction(self): @@ -108,6 +115,32 @@ def __invert__(self): """ raise NotImplementedError # :nocov: + # TODO(amaranth-0.6): make abstract + # @abstractmethod + def __add__(self, other): + """Concatenates two library I/O ports of the same type. + + The direction of the resulting port is: + + * The same as the direction of both, if the two ports have the same direction. + * :attr:`Direction.Input` if a bidirectional port is concatenated with an input port. + * :attr:`Direction.Output` if a bidirectional port is concatenated with an output port. + + Returns + ------- + :py:`type(self)` + A new :py:`type(self)` which contains wires from :py:`self` followed by wires + from :py:`other`, preserving their polarity inversion. + + Raises + ------ + :exc:`ValueError` + If an input port is concatenated with an output port. + :exc:`TypeError` + If :py:`self` and :py:`other` have different types. + """ + raise NotImplementedError # :nocov: + class SingleEndedPort(PortLike): """Represents a single-ended library I/O port. @@ -124,9 +157,9 @@ class SingleEndedPort(PortLike): same length as the width of :py:`io`, and the inversion is specified for individual wires. direction : :class:`Direction` or :class:`str` Set of allowed buffer directions. A string is converted to a :class:`Direction` first. - If equal to :attr:`Direction.Input` or :attr:`Direction.Output`, this port can only be used - with buffers of matching direction. If equal to :attr:`Direction.Bidir`, this port can be - used with buffers of any direction. + If equal to :attr:`~Direction.Input` or :attr:`~Direction.Output`, this port can only be + used with buffers of matching direction. If equal to :attr:`~Direction.Bidir`, this port + can be used with buffers of any direction. Attributes ---------- @@ -176,27 +209,6 @@ def __getitem__(self, index): direction=self._direction) def __add__(self, other): - """Concatenates two single-ended library I/O ports. - - The direction of the resulting port is: - - * The same as the direction of both, if the two ports have the same direction. - * :attr:`Direction.Input` if a bidirectional port is concatenated with an input port. - * :attr:`Direction.Output` if a bidirectional port is concatenated with an output port. - - Returns - ------- - :class:`SingleEndedPort` - A new :class:`SingleEndedPort` which contains wires from :py:`self` followed by wires - from :py:`other`, preserving their polarity inversion. - - Raises - ------ - :exc:`ValueError` - If an input port is concatenated with an output port. - :exc:`TypeError` - If :py:`self` and :py:`other` have incompatible types. - """ if not isinstance(other, SingleEndedPort): return NotImplemented return SingleEndedPort(Cat(self._io, other._io), invert=self._invert + other._invert, @@ -231,9 +243,9 @@ class DifferentialPort(PortLike): individual wires. direction : :class:`Direction` or :class:`str` Set of allowed buffer directions. A string is converted to a :class:`Direction` first. - If equal to :attr:`Direction.Input` or :attr:`Direction.Output`, this port can only be used - with buffers of matching direction. If equal to :attr:`Direction.Bidir`, this port can be - used with buffers of any direction. + If equal to :attr:`~Direction.Input` or :attr:`~Direction.Output`, this port can only be + used with buffers of matching direction. If equal to :attr:`~Direction.Bidir`, this port + can be used with buffers of any direction. Attributes ---------- @@ -293,27 +305,6 @@ def __getitem__(self, index): direction=self._direction) def __add__(self, other): - """Concatenates two differential library I/O ports. - - The direction of the resulting port is: - - * The same as the direction of both, if the two ports have the same direction. - * :attr:`Direction.Input` if a bidirectional port is concatenated with an input port. - * :attr:`Direction.Output` if a bidirectional port is concatenated with an output port. - - Returns - ------- - :class:`DifferentialPort` - A new :class:`DifferentialPort` which contains pairs of wires from :py:`self` followed - by pairs of wires from :py:`other`, preserving their polarity inversion. - - Raises - ------ - :exc:`ValueError` - If an input port is concatenated with an output port. - :exc:`TypeError` - If :py:`self` and :py:`other` have incompatible types. - """ if not isinstance(other, DifferentialPort): return NotImplemented return DifferentialPort(Cat(self._p, other._p), Cat(self._n, other._n), @@ -331,6 +322,167 @@ def __repr__(self): f"direction={self._direction})") +class SimulationPort(PortLike): + """Represents a simulation library I/O port. + + Implements the :class:`PortLike` interface. + + Parameters + ---------- + direction : :class:`Direction` or :class:`str` + Set of allowed buffer directions. A string is converted to a :class:`Direction` first. + If equal to :attr:`~Direction.Input` or :attr:`~Direction.Output`, this port can only be + used with buffers of matching direction. If equal to :attr:`~Direction.Bidir`, this port + can be used with buffers of any direction. + width : :class:`int` + Width of the port. The width of each of the attributes :py:`i`, :py:`o`, :py:`oe` (whenever + present) equals :py:`width`. + invert : :class:`bool` or iterable of :class:`bool` + Polarity inversion. If the value is a simple :class:`bool`, it specifies inversion for + the entire port. If the value is an iterable of :class:`bool`, the iterable must have the + same length as the width of :py:`p` and :py:`n`, and the inversion is specified for + individual wires. + name : :class:`str` or :py:`None` + Name of the port. This name is only used to derive the names of the input, output, and + output enable signals. + src_loc_at : :class:`int` + :ref:`Source location <lang-srcloc>`. Used to infer :py:`name` if not specified. + + Attributes + ---------- + i : :class:`Signal` + Input signal. Present if :py:`direction in (Input, Bidir)`. + o : :class:`Signal` + Ouptut signal. Present if :py:`direction in (Output, Bidir)`. + oe : :class:`Signal` + Output enable signal. Present if :py:`direction in (Output, Bidir)`. + invert : :class:`tuple` of :class:`bool` + The :py:`invert` parameter, normalized to specify polarity inversion per-wire. + direction : :class:`Direction` + The :py:`direction` parameter, normalized to the :class:`Direction` enumeration. + """ + def __init__(self, direction, width, *, invert=False, name=None, src_loc_at=0): + if name is not None and not isinstance(name, str): + raise TypeError(f"Name must be a string, not {name!r}") + if name is None: + name = tracer.get_var_name(depth=2 + src_loc_at, default="$port") + + if not (isinstance(width, int) and width >= 0): + raise TypeError(f"Width must be a non-negative integer, not {width!r}") + + self._direction = Direction(direction) + + self._i = self._o = self._oe = None + if self._direction in (Direction.Input, Direction.Bidir): + self._i = Signal(width, name=f"{name}__i") + if self._direction in (Direction.Output, Direction.Bidir): + self._o = Signal(width, name=f"{name}__o") + self._oe = Signal(width, name=f"{name}__oe", + init=~0 if self._direction is Direction.Output else 0) + + if isinstance(invert, bool): + self._invert = (invert,) * width + elif isinstance(invert, Iterable): + self._invert = tuple(invert) + if len(self._invert) != width: + raise ValueError(f"Length of 'invert' ({len(self._invert)}) doesn't match " + f"port width ({width})") + if not all(isinstance(item, bool) for item in self._invert): + raise TypeError(f"'invert' must be a bool or iterable of bool, not {invert!r}") + else: + raise TypeError(f"'invert' must be a bool or iterable of bool, not {invert!r}") + + @property + def i(self): + if self._i is None: + raise AttributeError( + "Simulation port with output direction does not have an input signal") + return self._i + + @property + def o(self): + if self._o is None: + raise AttributeError( + "Simulation port with input direction does not have an output signal") + return self._o + + @property + def oe(self): + if self._oe is None: + raise AttributeError( + "Simulation port with input direction does not have an output enable signal") + return self._oe + + @property + def invert(self): + return self._invert + + @property + def direction(self): + return self._direction + + def __len__(self): + if self._direction is Direction.Input: + return len(self._i) + if self._direction is Direction.Output: + assert len(self._o) == len(self._oe) + return len(self._o) + if self._direction is Direction.Bidir: + assert len(self._i) == len(self._o) == len(self._oe) + return len(self._i) + assert False # :nocov: + + def __getitem__(self, key): + result = object.__new__(type(self)) + result._i = None if self._i is None else self._i [key] + result._o = None if self._o is None else self._o [key] + result._oe = None if self._oe is None else self._oe[key] + if isinstance(key, slice): + result._invert = self._invert[key] + else: + result._invert = (self._invert[key],) + result._direction = self._direction + return result + + def __invert__(self): + result = object.__new__(type(self)) + result._i = self._i + result._o = self._o + result._oe = self._oe + result._invert = tuple(not invert for invert in self._invert) + result._direction = self._direction + return result + + def __add__(self, other): + if not isinstance(other, SimulationPort): + return NotImplemented + direction = self._direction & other._direction + result = object.__new__(type(self)) + result._i = None if direction is Direction.Output else Cat(self._i, other._i) + result._o = None if direction is Direction.Input else Cat(self._o, other._o) + result._oe = None if direction is Direction.Input else Cat(self._oe, other._oe) + result._invert = self._invert + other._invert + result._direction = direction + return result + + def __repr__(self): + parts = [] + if self._i is not None: + parts.append(f"i={self._i!r}") + if self._o is not None: + parts.append(f"o={self._o!r}") + if self._oe is not None: + parts.append(f"oe={self._oe!r}") + if not any(self._invert): + invert = False + elif all(self._invert): + invert = True + else: + invert = self._invert + return (f"SimulationPort({', '.join(parts)}, invert={invert!r}, " + f"direction={self._direction})") + + class Buffer(wiring.Component): """A combinational I/O buffer component. @@ -476,6 +628,18 @@ def elaborate(self, platform): else: m.submodules += IOBufferInstance(self._port.p, o=o_inv, oe=self.oe, i=i_inv) m.submodules += IOBufferInstance(self._port.n, o=~o_inv, oe=self.oe) + elif isinstance(self._port, SimulationPort): + if self.direction is Direction.Bidir: + # Loop back `o` if `oe` is asserted. This frees the test harness from having to + # provide this functionality itself. + for i_inv_bit, oe_bit, o_bit, i_bit in \ + zip(i_inv, self._port.oe, self._port.o, self._port.i): + m.d.comb += i_inv_bit.eq(Cat(Mux(oe_bit, o_bit, i_bit))) + if self.direction is Direction.Input: + m.d.comb += i_inv.eq(self._port.i) + if self.direction in (Direction.Output, Direction.Bidir): + m.d.comb += self._port.o.eq(o_inv) + m.d.comb += self._port.oe.eq(self.oe.replicate(len(self._port))) else: raise TypeError("Cannot elaborate generic 'Buffer' with port {self._port!r}") # :nocov: @@ -719,6 +883,12 @@ class DDRBuffer(wiring.Component): This limitation may be lifted in the future. + .. warning:: + + Double data rate I/O buffers are not compatible with :class:`SimulationPort`. + + This limitation may be lifted in the future. + Parameters ---------- direction : :class:`Direction` @@ -826,6 +996,9 @@ def elaborate(self, platform): if hasattr(platform, "get_io_buffer"): return platform.get_io_buffer(self) + if isinstance(self._port, SimulationPort): + raise NotImplementedError(f"DDR buffers are not supported in simulation") # :nocov: + raise NotImplementedError(f"DDR buffers are not supported on {platform!r}") # :nocov: diff --git a/amaranth/lib/stream.py b/amaranth/lib/stream.py index 2cbf0d422..a24f25bcb 100644 --- a/amaranth/lib/stream.py +++ b/amaranth/lib/stream.py @@ -19,7 +19,9 @@ class Signature(wiring.Signature): Parameters ---------- payload_shape : :class:`~.hdl.ShapeLike` - Shape of the payload. + Shape of the payload member. + payload_init : :ref:`constant-castable <lang-constcasting>` object + Initial value of the payload member. always_valid : :class:`bool` Whether the stream has a payload available each cycle. always_ready : :class:`bool` @@ -35,14 +37,15 @@ class Signature(wiring.Signature): ready : :py:`In(1)` Whether a payload is accepted. If the stream is :py:`always_ready`, :py:`Const(1)`. """ - def __init__(self, payload_shape: ShapeLike, *, always_valid=False, always_ready=False): + def __init__(self, payload_shape: ShapeLike, *, payload_init=None, + always_valid=False, always_ready=False): Shape.cast(payload_shape) self._payload_shape = payload_shape self._always_valid = bool(always_valid) self._always_ready = bool(always_ready) super().__init__({ - "payload": Out(payload_shape), + "payload": Out(payload_shape, init=payload_init), "valid": Out(1), "ready": In(1) }) @@ -119,4 +122,4 @@ def p(self): def __repr__(self): return (f"stream.Interface(payload={self.payload!r}, valid={self.valid!r}, " - f"ready={self.ready!r})") \ No newline at end of file + f"ready={self.ready!r})") diff --git a/amaranth/lib/wiring.py b/amaranth/lib/wiring.py index 1bf963e96..1122a625f 100644 --- a/amaranth/lib/wiring.py +++ b/amaranth/lib/wiring.py @@ -5,6 +5,7 @@ from .. import tracer from ..hdl._ast import Shape, ShapeCastable, Const, Signal, Value, ValueCastable +from ..hdl._dsl import Module from ..hdl._ir import Elaboratable from .._utils import final from .meta import Annotation, InvalidAnnotation @@ -825,8 +826,8 @@ def is_compliant(self, obj, *, reasons=None, path=("obj",)): satisfying the requirements below; * for port members, is a :ref:`value-like <lang-valuelike>` object casting to a :class:`Signal` or a :class:`Const` whose width and signedness is the same as that - of the member, and (in case of a :class:`Signal`) which is not reset-less and whose - initial value is that of the member; + of the member, and (in case of a :class:`Signal`) whose initial value is that of the + member; * for signature members, matches the description in the signature as verified by :meth:`Signature.is_compliant`. @@ -1198,7 +1199,7 @@ def __repr__(self): attrs = ''.join(f", {name}={value!r}" for name, value in self.__dict__.items() if name != "signature") - return f'<{type(self).__name__}: {self.signature}{attrs}>' + return f'<{type(self).__qualname__}: {self.signature}{attrs}>' # To reduce API surface area `FlippedInterface` is made final. This restriction could be lifted @@ -1401,6 +1402,9 @@ def connect(m, *args, **kwargs): a connection cannot be made. """ + if not isinstance(m, Module): + raise TypeError(f"The initial argument must be a module, not {m!r}") + objects = { **{index: arg for index, arg in enumerate(args)}, **{keyword: arg for keyword, arg in kwargs.items()} diff --git a/amaranth/rpc.py b/amaranth/rpc.py index af0f125e0..f8149d1aa 100644 --- a/amaranth/rpc.py +++ b/amaranth/rpc.py @@ -3,6 +3,8 @@ import argparse import importlib +from amaranth.lib.wiring import Signature + from .hdl import Signal, Record, Elaboratable from .back import rtlil @@ -68,16 +70,18 @@ def _serve_yosys(modules): try: elaboratable = modules[module_name](*args, **kwargs) - ports = [] - # By convention, any public attribute that is a Signal or a Record is - # considered a port. - for port_name, port in vars(elaboratable).items(): - if not port_name.startswith("_") and isinstance(port, (Signal, Record)): - ports += port._lhs_signals() + ports = None + if not (hasattr(elaboratable, "signature") and isinstance(elaboratable.signature, Signature)): + ports = [] + # By convention, any public attribute that is a Signal or a Record is + # considered a port. + for port_name, port in vars(elaboratable).items(): + if not port_name.startswith("_") and isinstance(port, (Signal, Record)): + ports += port._lhs_signals() rtlil_text = rtlil.convert(elaboratable, name=module_name, ports=ports) - response = {"frontend": "ilang", "source": rtlil_text} + response = {"frontend": "rtlil", "source": rtlil_text} except Exception as error: - response = {"error": f"{type(error).__name__}: {str(error)}"} + response = {"error": f"{type(error).__qualname__}: {str(error)}"} else: return {"error": "Unrecognized method {!r}".format(request["method"])} diff --git a/amaranth/sim/_base.py b/amaranth/sim/_base.py index c63b95d81..7e58112a4 100644 --- a/amaranth/sim/_base.py +++ b/amaranth/sim/_base.py @@ -23,7 +23,7 @@ class BaseSignalState: curr = NotImplemented next = NotImplemented - def update(self, value): + def update(self, value, mask=~0): raise NotImplementedError # :nocov: diff --git a/amaranth/sim/_pyrtl.py b/amaranth/sim/_pyrtl.py index be624fa6b..0c94421ee 100644 --- a/amaranth/sim/_pyrtl.py +++ b/amaranth/sim/_pyrtl.py @@ -5,7 +5,7 @@ from ..hdl import * from ..hdl._ast import SignalSet, _StatementList, Property -from ..hdl._xfrm import ValueVisitor, StatementVisitor +from ..hdl._xfrm import ValueVisitor, StatementVisitor, LHSMaskCollector from ..hdl._mem import MemoryInstance from ._base import BaseProcess from ._pyeval import value_to_string @@ -487,19 +487,20 @@ def __call__(self, fragment): for domain_name in domains: domain_stmts = fragment.statements.get(domain_name, _StatementList()) domain_process = PyRTLProcess(is_comb=domain_name == "comb") - domain_signals = domain_stmts._lhs_signals() + lhs_masks = LHSMaskCollector() + lhs_masks.visit_stmt(domain_stmts) if isinstance(fragment, MemoryInstance): for port in fragment._read_ports: if port._domain == domain_name: - domain_signals.update(port._data._lhs_signals()) + lhs_masks.visit_value(port._data, ~0) emitter = _PythonEmitter() emitter.append(f"def run():") emitter._level += 1 if domain_name == "comb": - for signal in domain_signals: + for (signal, _) in lhs_masks.masks(): signal_index = self.state.get_signal(signal) self.state.slots[signal_index].is_comb = True emitter.append(f"next_{signal_index} = {signal.init}") @@ -533,7 +534,7 @@ def __call__(self, fragment): if domain.async_reset and domain.rst is not None: self.state.add_signal_waker(domain.rst, edge_waker(domain_process, 1)) - for signal in domain_signals: + for (signal, _) in lhs_masks.masks(): signal_index = self.state.get_signal(signal) emitter.append(f"next_{signal_index} = slots[{signal_index}].next") @@ -546,7 +547,7 @@ def __call__(self, fragment): emitter.append(f"if {rst}:") with emitter.indent(): emitter.append("pass") - for signal in domain_signals: + for (signal, _) in lhs_masks.masks(): if not signal.reset_less: signal_index = self.state.get_signal(signal) emitter.append(f"next_{signal_index} = {signal.init}") @@ -592,9 +593,11 @@ def __call__(self, fragment): lhs(port._data)(data) - for signal in domain_signals: + for (signal, mask) in lhs_masks.masks(): + if signal.shape().signed and (mask & 1 << (len(signal) - 1)): + mask |= -1 << len(signal) signal_index = self.state.get_signal(signal) - emitter.append(f"slots[{signal_index}].update(next_{signal_index})") + emitter.append(f"slots[{signal_index}].update(next_{signal_index}, {mask})") # There shouldn't be any exceptions raised by the generated code, but if there are # (almost certainly due to a bug in the code generator), use this environment variable diff --git a/amaranth/sim/core.py b/amaranth/sim/core.py index 5c5039939..2c9bc575a 100644 --- a/amaranth/sim/core.py +++ b/amaranth/sim/core.py @@ -132,6 +132,18 @@ def add_clock(self, period, *, phase=None, domain="sync", if_exists=False): @staticmethod def _check_function(function, *, kind): + if inspect.isasyncgenfunction(function): + raise TypeError( + f"Cannot add a {kind} {function!r} because it is an async generator function " + f"(there is likely a stray `yield` in the function)") + if inspect.iscoroutine(function): + raise TypeError( + f"Cannot add a {kind} {function!r} because it is a coroutine object instead " + f"of a function (pass the function itself instead of calling it)") + if inspect.isgenerator(function) or inspect.isasyncgen(function): + raise TypeError( + f"Cannot add a {kind} {function!r} because it is a generator object instead " + f"of a function (pass the function itself instead of calling it)") if not (inspect.isgeneratorfunction(function) or inspect.iscoroutinefunction(function)): raise TypeError( f"Cannot add a {kind} {function!r} because it is not an async function or " diff --git a/amaranth/sim/pysim.py b/amaranth/sim/pysim.py index 2cc646e6e..ddfb8ad2e 100644 --- a/amaranth/sim/pysim.py +++ b/amaranth/sim/pysim.py @@ -269,24 +269,22 @@ def close(self, timestamp): self.gtkw_save.treeopen("top") def traverse_traces(traces): - if isinstance(traces, Signal): - for name in self.gtkw_signal_names[traces]: - self.gtkw_save.trace(name) - elif isinstance(traces, data.View): + if isinstance(traces, data.View): with self.gtkw_save.group("view"): - trace = Value.cast(traces) + traverse_traces(Value.cast(traces)) + elif isinstance(traces, ValueLike): + trace = Value.cast(traces) + if isinstance(traces, MemoryData._Row): + for name in self.gtkw_memory_names[traces._memory][traces._index]: + self.gtkw_save.trace(name) + else: for trace_signal in trace._rhs_signals(): for name in self.gtkw_signal_names[trace_signal]: self.gtkw_save.trace(name) - elif isinstance(traces, ValueLike): - traverse_traces(Value.cast(traces)) elif isinstance(traces, MemoryData): for row_names in self.gtkw_memory_names[traces]: for name in row_names: self.gtkw_save.trace(name) - elif isinstance(traces, MemoryData._Row): - for name in self.gtkw_memory_names[traces._memory][traces._index]: - self.gtkw_save.trace(name) elif hasattr(traces, "signature") and isinstance(traces.signature, wiring.Signature): with self.gtkw_save.group("interface"): for _, _, member in traces.signature.flatten(traces): @@ -369,7 +367,8 @@ def add_waker(self, waker): assert waker not in self.wakers self.wakers.append(waker) - def update(self, value): + def update(self, value, mask=~0): + value = (self.next & ~mask) | (value & mask) if self.next != value: self.next = value self.pending.add(self) @@ -393,10 +392,11 @@ def __init__(self, state, addr): class _PyMemoryState(BaseMemoryState): - __slots__ = ("memory", "data", "write_queue", "wakers", "pending") + __slots__ = ("memory", "shape", "data", "write_queue", "wakers", "pending") def __init__(self, memory, pending): self.memory = memory + self.shape = Shape.cast(memory.shape) self.pending = pending self.wakers = list() self.reset() @@ -420,6 +420,11 @@ def write(self, addr, value, mask=None): self.write_queue[addr] = self.data[addr] if mask is not None: value = (value & mask) | (self.write_queue[addr] & ~mask) + if self.shape.signed: + if value & (1 << (self.shape.width - 1)): + value |= -1 << (self.shape.width) + else: + value &= (1 << (self.shape.width)) - 1 self.write_queue[addr] = value self.pending.add(self) diff --git a/amaranth/vendor/_altera.py b/amaranth/vendor/_altera.py index 8da033e96..679a48527 100644 --- a/amaranth/vendor/_altera.py +++ b/amaranth/vendor/_altera.py @@ -222,7 +222,7 @@ class AlteraPlatform(TemplatedPlatform): * ``verbose``: enables logging of informational messages to standard error. * ``read_verilog_opts``: adds options for ``read_verilog`` Yosys command. * ``synth_opts``: adds options for ``synth_intel_alm`` Yosys command. - * ``script_after_read``: inserts commands after ``read_ilang`` in Yosys script. + * ``script_after_read``: inserts commands after ``read_rtlil`` in Yosys script. * ``script_after_synth``: inserts commands after ``synth_intel_alm`` in Yosys script. * ``yosys_opts``: adds extra options for ``yosys``. * ``nextpnr_opts``: adds extra options for ``nextpnr-mistral``. @@ -310,12 +310,11 @@ class AlteraPlatform(TemplatedPlatform): {{get_override("add_settings")|default("# (add_settings placeholder)")}} """, "{{name}}.sdc": r""" - {% for net_signal, port_signal, frequency in platform.iter_clock_constraints() -%} - {% if port_signal is not none -%} - create_clock -name {{port_signal.name|tcl_quote}} -period {{1000000000/frequency}} [get_ports {{port_signal.name|tcl_quote}}] - {% else -%} - create_clock -name {{net_signal.name|tcl_quote}} -period {{1000000000/frequency}} [get_nets {{net_signal|hierarchy("|")|tcl_quote}}] - {% endif %} + {% for signal, frequency in platform.iter_signal_clock_constraints() -%} + create_clock -name {{signal.name|tcl_quote}} -period {{1000000000/frequency}} [get_nets {{signal|hierarchy("|")|tcl_quote}}] + {% endfor %} + {% for port, frequency in platform.iter_port_clock_constraints() -%} + create_clock -name {{port.name|tcl_quote}} -period {{1000000000/frequency}} [get_ports {{port.name|tcl_quote}}] {% endfor %} {{get_override("add_constraints")|default("# (add_constraints placeholder)")}} """, @@ -374,9 +373,9 @@ class AlteraPlatform(TemplatedPlatform): read_verilog -sv {{get_override("read_verilog_opts")|options}} {{file}} {% endfor %} {% for file in platform.iter_files(".il") -%} - read_ilang {{file}} + read_rtlil {{file}} {% endfor %} - read_ilang {{name}}.il + read_rtlil {{name}}.il {{get_override("script_after_read")|default("# (script_after_read placeholder)")}} synth_intel_alm {{get_override("synth_opts")|options}} -top {{name}} {{get_override("script_after_synth")|default("# (script_after_synth placeholder)")}} diff --git a/amaranth/vendor/_gowin.py b/amaranth/vendor/_gowin.py index 11c4fbf7e..fa063c034 100644 --- a/amaranth/vendor/_gowin.py +++ b/amaranth/vendor/_gowin.py @@ -376,9 +376,9 @@ def _osc_div(self): read_verilog -sv {{get_override("read_verilog_opts")|options}} {{file}} {% endfor %} {% for file in platform.iter_files(".il") -%} - read_ilang {{file}} + read_rtlil {{file}} {% endfor %} - read_ilang {{name}}.il + read_rtlil {{name}}.il {{get_override("script_after_read")|default("# (script_after_read placeholder)")}} synth_gowin {{get_override("synth_opts")|options}} -top {{name}} -json {{name}}.syn.json {{get_override("script_after_synth")|default("# (script_after_synth placeholder)")}} @@ -437,12 +437,18 @@ def _osc_div(self): file delete -force {{name}}.fs file copy -force impl/pnr/project.fs {{name}}.fs """, + # Gowin is using neither Tcl nor the Synopsys code to parse SDC files, so the grammar + # deviates from the usual (eg. no quotes, no nested braces). "{{name}}.sdc": r""" // {{autogenerated}} - {% for net_signal,port_signal,frequency in platform.iter_clock_constraints() -%} - create_clock -name {{port_signal.name|tcl_quote}} -period {{1000000000/frequency}} [get_ports {{port_signal.name|tcl_quote}}] + {% for signal, frequency in platform.iter_signal_clock_constraints() -%} + create_clock -name {{ "{" }}{{signal.name}}{{ "}" }} -period {{1000000000/frequency}} [get_nets {{signal|hierarchy("/")|tcl_quote}}] {% endfor %} - {{get_override("add_constraints")|default("# (add_constraints placeholder)")}} + {% for port, frequency in platform.iter_port_clock_constraints() -%} + create_clock -name {{ "{" }}{{port.name}}{{ "}" }} -period {{1000000000/frequency}} [get_ports + {{ "{" }}{{port.name}}{{ "}" }}] + {% endfor %} + {{get_override("add_constraints")|default("// (add_constraints placeholder)")}} """, } _gowin_command_templates = [ @@ -529,10 +535,14 @@ def create_missing_domain(self, name): o_OSCOUT=clk_i) else: - clk_i = self.request(self.default_clk).i + clk_io = self.request(self.default_clk, dir="-") + m.submodules.clk_buf = clk_buf = io.Buffer("i", clk_io) + clk_i = clk_buf.i if self.default_rst is not None: - rst_i = self.request(self.default_rst).i + rst_io = self.request(self.default_rst, dir="-") + m.submodules.rst_buf = rst_buf = io.Buffer("i", rst_io) + rst_i = rst_buf.i else: rst_i = Const(0) diff --git a/amaranth/vendor/_lattice.py b/amaranth/vendor/_lattice.py index 834926f4d..04650dd61 100644 --- a/amaranth/vendor/_lattice.py +++ b/amaranth/vendor/_lattice.py @@ -319,7 +319,7 @@ class LatticePlatform(TemplatedPlatform): * ``verbose``: enables logging of informational messages to standard error. * ``read_verilog_opts``: adds options for ``read_verilog`` Yosys command. * ``synth_opts``: adds options for ``synth_<family>`` Yosys command. - * ``script_after_read``: inserts commands after ``read_ilang`` in Yosys script. + * ``script_after_read``: inserts commands after ``read_rtlil`` in Yosys script. * ``script_after_synth``: inserts commands after ``synth_<family>`` in Yosys script. * ``yosys_opts``: adds extra options for ``yosys``. * ``nextpnr_opts``: adds extra options for ``nextpnr-<family>``. @@ -348,7 +348,7 @@ class LatticePlatform(TemplatedPlatform): * ``verbose``: enables logging of informational messages to standard error. * ``read_verilog_opts``: adds options for ``read_verilog`` Yosys command. * ``synth_opts``: adds options for ``synth_nexus`` Yosys command. - * ``script_after_read``: inserts commands after ``read_ilang`` in Yosys script. + * ``script_after_read``: inserts commands after ``read_rtlil`` in Yosys script. * ``script_after_synth``: inserts commands after ``synth_nexus`` in Yosys script. * ``yosys_opts``: adds extra options for ``yosys``. * ``nextpnr_opts``: adds extra options for ``nextpnr-nexus``. @@ -474,9 +474,9 @@ class LatticePlatform(TemplatedPlatform): read_verilog -sv {{get_override("read_verilog_opts")|options}} {{file}} {% endfor %} {% for file in platform.iter_files(".il") -%} - read_ilang {{file}} + read_rtlil {{file}} {% endfor %} - read_ilang {{name}}.il + read_rtlil {{name}}.il {{get_override("script_after_read")|default("# (script_after_read placeholder)")}} {% if platform.family == "ecp5" %} synth_ecp5 {{get_override("synth_opts")|options}} -top {{name}} @@ -497,12 +497,11 @@ class LatticePlatform(TemplatedPlatform): {%- for key, value in attrs.items() %} {{key}}={{value}}{% endfor %}; {% endif %} {% endfor %} - {% for net_signal, port_signal, frequency in platform.iter_clock_constraints() -%} - {% if port_signal is not none -%} - FREQUENCY PORT "{{port_signal.name}}" {{frequency}} HZ; - {% else -%} - FREQUENCY NET "{{net_signal|hierarchy(".")}}" {{frequency}} HZ; - {% endif %} + {% for signal, frequency in platform.iter_signal_clock_constraints() -%} + FREQUENCY NET "{{signals|hierarchy(".")}}" {{frequency}} HZ; + {% endfor %} + {% for port, frequency in platform.iter_port_clock_constraints() -%} + FREQUENCY PORT "{{port.name}}" {{frequency}} HZ; {% endfor %} {{get_override("add_preferences")|default("# (add_preferences placeholder)")}} """ @@ -567,9 +566,9 @@ class LatticePlatform(TemplatedPlatform): read_verilog -sv {{get_override("read_verilog_opts")|options}} {{file}} {% endfor %} {% for file in platform.iter_files(".il") -%} - read_ilang {{file}} + read_rtlil {{file}} {% endfor %} - read_ilang {{name}}.il + read_rtlil {{name}}.il delete w:$verilog_initial_trigger {{get_override("script_after_read")|default("# (script_after_read placeholder)")}} synth_nexus {{get_override("synth_opts")|options}} -top {{name}} @@ -584,12 +583,11 @@ class LatticePlatform(TemplatedPlatform): ldc_set_port -iobuf {{ '{' }}{%- for key, value in attrs.items() %}{{key}}={{value}} {% endfor %}{{ '}' }} {{'['}}get_ports {{port_name}}{{']'}} {% endif %} {% endfor %} - {% for net_signal, port_signal, frequency in platform.iter_clock_constraints() -%} - {% if port_signal is not none -%} - create_clock -name {{port_signal.name|tcl_quote}} -period {{1000000000/frequency}} [get_ports {{port_signal.name|tcl_quote}}] - {% else -%} - create_clock -name {{net_signal.name|tcl_quote}} -period {{1000000000/frequency}} [get_nets {{net_signal|hierarchy("/")|tcl_quote}}] - {% endif %} + {% for signal, frequency in platform.iter_signal_clock_constraints() -%} + create_clock -name {{signal.name|tcl_quote}} -period {{1000000000/frequency}} [get_nets {{signal|hierarchy("/")|tcl_quote}}] + {% endfor %} + {% for port, frequency in platform.iter_port_clock_constraints() -%} + create_clock -name {{port.name|tcl_quote}} -period {{1000000000/frequency}} [get_ports {{port.name|tcl_quote}}] {% endfor %} {{get_override("add_preferences")|default("# (add_preferences placeholder)")}} """ @@ -684,12 +682,11 @@ class LatticePlatform(TemplatedPlatform): """, "{{name}}.sdc": r""" set_hierarchy_separator {/} - {% for net_signal, port_signal, frequency in platform.iter_clock_constraints() -%} - {% if port_signal is not none -%} - create_clock -name {{port_signal.name|tcl_quote("Diamond")}} -period {{1000000000/frequency}} [get_ports {{port_signal.name|tcl_quote("Diamond")}}] - {% else -%} - create_clock -name {{net_signal.name|tcl_quote("Diamond")}} -period {{1000000000/frequency}} [get_nets {{net_signal|hierarchy("/")|tcl_quote("Diamond")}}] - {% endif %} + {% for signal, frequency in platform.iter_signal_clock_constraints() -%} + create_clock -name {{signal.name|tcl_quote("Diamond")}} -period {{1000000000/frequency}} [get_nets {{signal|hierarchy("/")|tcl_quote("Diamond")}}] + {% endfor %} + {% for port, frequency in platform.iter_port_clock_constraints() -%} + create_clock -name {{port.name|tcl_quote("Diamond")}} -period {{1000000000/frequency}} [get_ports {{port.name|tcl_quote("Diamond")}}] {% endfor %} {{get_override("add_constraints")|default("# (add_constraints placeholder)")}} """, @@ -782,12 +779,11 @@ class LatticePlatform(TemplatedPlatform): """, # Pre-synthesis SDC constraints "{{name}}.sdc": r""" - {% for net_signal, port_signal, frequency in platform.iter_clock_constraints() -%} - {% if port_signal is not none -%} - create_clock -name {{port_signal.name|tcl_quote}} -period {{1000000000/frequency}} [get_ports {{port_signal.name}}] - {% else -%} - create_clock -name {{net_signal.name|tcl_quote}} -period {{1000000000/frequency}} [get_nets {{net_signal|hierarchy("/")}}] - {% endif %} + {% for signal, frequency in platform.iter_signal_clock_constraints() -%} + create_clock -name {{signal.name|tcl_quote}} -period {{1000000000/frequency}} [get_nets {{signal|hierarchy("/")|tcl_quote}}] + {% endfor %} + {% for port, frequency in platform.iter_port_clock_constraints() -%} + create_clock -name {{port.name|tcl_quote}} -period {{1000000000/frequency}} [get_ports {{port.name|tcl_quote}}] {% endfor %} {{get_override("add_constraints")|default("# (add_constraints placeholder)")}} """, @@ -960,9 +956,14 @@ def create_missing_domain(self, name): o_HFCLKOUT=clk_i, ) else: - clk_i = self.request(self.default_clk).i + clk_io = self.request(self.default_clk, dir="-") + m.submodules.clk_buf = clk_buf = io.Buffer("i", clk_io) + clk_i = clk_buf.i + if self.default_rst is not None: - rst_i = self.request(self.default_rst).i + rst_io = self.request(self.default_rst, dir="-") + m.submodules.rst_buf = rst_buf = io.Buffer("i", rst_io) + rst_i = rst_buf.i else: rst_i = Const(0) diff --git a/amaranth/vendor/_quicklogic.py b/amaranth/vendor/_quicklogic.py index 33a490e41..c7809b9a6 100644 --- a/amaranth/vendor/_quicklogic.py +++ b/amaranth/vendor/_quicklogic.py @@ -1,6 +1,7 @@ from abc import abstractmethod from ..hdl import * +from ..lib import io from ..lib.cdc import ResetSynchronizer from ..build import * @@ -71,10 +72,11 @@ class QuicklogicPlatform(TemplatedPlatform): """, "{{name}}.sdc": r""" # {{autogenerated}} - {% for net_signal, port_signal, frequency in platform.iter_clock_constraints() -%} - {% if port_signal is not none -%} - create_clock -period {{100000000/frequency}} {{port_signal.name|ascii_escape}} - {% endif %} + {% for signal, frequency in platform.iter_signal_clock_constraints() -%} + create_clock -period {{100000000/frequency}} {{signal.name|ascii_escape}} + {% endfor %} + {% for port, frequency in platform.iter_port_clock_constraints() -%} + create_clock -period {{100000000/frequency}} {{port.name|ascii_escape}} {% endfor %} """ } @@ -171,10 +173,14 @@ def create_missing_domain(self, name): o_A=sys_clk0, o_Z=clk_i) else: - clk_i = self.request(self.default_clk).i + clk_io = self.request(self.default_clk, dir="-") + m.submodules.clk_buf = clk_buf = io.Buffer("i", clk_io) + clk_i = clk_buf.i if self.default_rst is not None: - rst_i = self.request(self.default_rst).i + rst_io = self.request(self.default_rst, dir="-") + m.submodules.rst_buf = rst_buf = io.Buffer("i", rst_io) + rst_i = rst_buf.i else: rst_i = Const(0) diff --git a/amaranth/vendor/_siliconblue.py b/amaranth/vendor/_siliconblue.py index 0db838088..0d179c0ec 100644 --- a/amaranth/vendor/_siliconblue.py +++ b/amaranth/vendor/_siliconblue.py @@ -26,7 +26,7 @@ class SiliconBluePlatform(TemplatedPlatform): * ``verbose``: enables logging of informational messages to standard error. * ``read_verilog_opts``: adds options for ``read_verilog`` Yosys command. * ``synth_opts``: adds options for ``synth_ice40`` Yosys command. - * ``script_after_read``: inserts commands after ``read_ilang`` in Yosys script. + * ``script_after_read``: inserts commands after ``read_rtlil`` in Yosys script. * ``script_after_synth``: inserts commands after ``synth_ice40`` in Yosys script. * ``yosys_opts``: adds extra options for ``yosys``. * ``nextpnr_opts``: adds extra options for ``nextpnr-ice40``. @@ -121,9 +121,9 @@ class SiliconBluePlatform(TemplatedPlatform): read_verilog -sv {{get_override("read_verilog_opts")|options}} {{file}} {% endfor %} {% for file in platform.iter_files(".il") -%} - read_ilang {{file}} + read_rtlil {{file}} {% endfor %} - read_ilang {{name}}.il + read_rtlil {{name}}.il {{get_override("script_after_read")|default("# (script_after_read placeholder)")}} synth_ice40 {{get_override("synth_opts")|options}} -top {{name}} {{get_override("script_after_synth")|default("# (script_after_synth placeholder)")}} @@ -134,9 +134,12 @@ class SiliconBluePlatform(TemplatedPlatform): {% for port_name, pin_name, attrs in platform.iter_port_constraints_bits() -%} set_io {{port_name}} {{pin_name}} {% endfor %} - {% for net_signal, port_signal, frequency in platform.iter_clock_constraints() -%} - set_frequency {{net_signal|hierarchy(".")}} {{frequency/1000000}} - {% endfor%} + {% for signal, frequency in platform.iter_signal_clock_constraints() -%} + set_frequency {{signal|hierarchy(".")}} {{frequency/1000000}} + {% endfor %} + {% for port, frequency in platform.iter_port_clock_constraints() -%} + set_frequency {{port.name}} {{frequency/1000000}} + {% endfor %} {{get_override("add_constraints")|default("# (add_constraints placeholder)")}} """, } @@ -242,12 +245,11 @@ class SiliconBluePlatform(TemplatedPlatform): "{{name}}.sdc": r""" # {{autogenerated}} set_hierarchy_separator {/} - {% for net_signal, port_signal, frequency in platform.iter_clock_constraints() -%} - {% if port_signal is not none -%} - create_clock -name {{port_signal.name|tcl_quote}} -period {{1000000000/frequency}} [get_ports {{port_signal.name|tcl_quote}}] - {% else -%} - create_clock -name {{net_signal.name|tcl_quote}} -period {{1000000000/frequency}} [get_nets {{net_signal|hierarchy("/")|tcl_quote}}] - {% endif %} + {% for signal, frequency in platform.iter_signal_clock_constraints() -%} + create_clock -name {{signal.name|tcl_quote}} -period {{1000000000/frequency}} [get_nets {{signal|hierarchy("/")|tcl_quote}}] + {% endfor %} + {% for port, frequency in platform.iter_port_clock_constraints() -%} + create_clock -name {{port.name|tcl_quote}} -period {{1000000000/frequency}} [get_ports {{port.name|tcl_quote}}] {% endfor %} {{get_override("add_constraints")|default("# (add_constraints placeholder)")}} """, @@ -391,11 +393,15 @@ def create_missing_domain(self, name): delay = int(100e-6 * self.default_clk_frequency) # User-defined clock signal. else: - clk_i = self.request(self.default_clk).i + clk_io = self.request(self.default_clk, dir="-") + m.submodules.clk_buf = clk_buf = io.Buffer("i", clk_io) + clk_i = clk_buf.i delay = int(15e-6 * self.default_clk_frequency) if self.default_rst is not None: - rst_i = self.request(self.default_rst).i + rst_io = self.request(self.default_rst, dir="-") + m.submodules.rst_buf = rst_buf = io.Buffer("i", rst_io) + rst_i = rst_buf.i else: rst_i = Const(0) @@ -562,6 +568,10 @@ def get_io_buffer(self, buffer): raise TypeError("iCE40 does not support bidirectional differential ports") elif buffer.direction is io.Direction.Output: m = Module() + # Note that the non-inverting output pin is not driven the same way as a regular + # output pin. The inverter introduces a delay, so for a non-inverting output pin, + # an identical delay is introduced by instantiating a LUT. This makes the waveform + # perfectly symmetric in the xdr=0 case. invert_lut = isinstance(buffer, io.Buffer) m.submodules.p = self._get_io_buffer_single(buffer, port_p, invert_lut=invert_lut) m.submodules.n = self._get_io_buffer_single(buffer, port_n, invert_lut=invert_lut) @@ -572,7 +582,7 @@ def get_io_buffer(self, buffer): # differs between LP/HX and UP series: # * for LP/HX, z=0 is DPxxB (B is non-inverting, A is inverting) # * for UP, z=0 is IOB_xxA (A is non-inverting, B is inverting) - return self._get_io_buffer_single(buffer, port_p, invert_lut=invert_lut) + return self._get_io_buffer_single(buffer, port_p) else: assert False # :nocov: elif isinstance(buffer.port, io.SingleEndedPort): diff --git a/amaranth/vendor/_xilinx.py b/amaranth/vendor/_xilinx.py index 9a44c6253..1bc454e61 100644 --- a/amaranth/vendor/_xilinx.py +++ b/amaranth/vendor/_xilinx.py @@ -647,12 +647,11 @@ def vendor_toolchain(self): set_property {{attr_name}} {{attr_value|tcl_quote}} [get_ports {{port_name|tcl_quote}}] {% endfor %} {% endfor %} - {% for net_signal, port_signal, frequency in platform.iter_clock_constraints() -%} - {% if port_signal is not none -%} - create_clock -name {{port_signal.name|ascii_escape}} -period {{1000000000/frequency}} [get_ports {{port_signal.name|tcl_quote}}] - {% else -%} - create_clock -name {{net_signal.name|ascii_escape}} -period {{1000000000/frequency}} [get_nets {{net_signal|hierarchy("/")|tcl_quote}}] - {% endif %} + {% for signal, frequency in platform.iter_signal_clock_constraints() -%} + create_clock -name {{signal.name|ascii_escape}} -period {{1000000000/frequency}} [get_nets {{signal|hierarchy("/")|tcl_quote}}] + {% endfor %} + {% for port, frequency in platform.iter_port_clock_constraints() -%} + create_clock -name {{port.name|ascii_escape}} -period {{1000000000/frequency}} [get_nets {{port|hierarchy("/")|tcl_quote}}] {% endfor %} {{get_override("add_constraints")|default("# (add_constraints placeholder)")}} """ @@ -723,9 +722,13 @@ def vendor_toolchain(self): NET "{{port_name}}" {{attr_name}}={{attr_value}}; {% endfor %} {% endfor %} - {% for net_signal, port_signal, frequency in platform.iter_clock_constraints() -%} - NET "{{net_signal|hierarchy("/")}}" TNM_NET="PRD{{net_signal|hierarchy("/")}}"; - TIMESPEC "TS{{net_signal|hierarchy("__")}}"=PERIOD "PRD{{net_signal|hierarchy("/")}}" {{1000000000/frequency}} ns HIGH 50%; + {% for signal, frequency in platform.iter_signal_clock_constraints() -%} + NET "{{signal|hierarchy("/")}}" TNM_NET="PRD{{signal|hierarchy("/")}}"; + TIMESPEC "TS{{signal|hierarchy("__")}}"=PERIOD "PRD{{signal|hierarchy("/")}}" {{1000000000/frequency}} ns HIGH 50%; + {% endfor %} + {% for port, frequency in platform.iter_port_clock_constraints() -%} + NET "{{port|hierarchy("/")}}" TNM_NET="PRD{{port|hierarchy("/")}}"; + TIMESPEC "TS{{port|hierarchy("__")}}"=PERIOD "PRD{{port|hierarchy("/")}}" {{1000000000/frequency}} ns HIGH 50%; {% endfor %} {{get_override("add_constraints")|default("# (add_constraints placeholder)")}} """ @@ -848,10 +851,11 @@ def _symbiflow_device(self): """, "{{name}}.sdc": r""" # {{autogenerated}} - {% for net_signal, port_signal, frequency in platform.iter_clock_constraints() -%} - {% if port_signal is none -%} - create_clock -period {{1000000000/frequency}} {{net_signal.name|ascii_escape}} - {% endif %} + {% for signal, frequency in platform.iter_signal_clock_constraints() -%} + create_clock -period {{1000000000/frequency}} {{signal.name|ascii_escape}} + {% endfor %} + {% for port, frequency in platform.iter_port_clock_constraints() -%} + create_clock -period {{1000000000/frequency}} {{port.name|ascii_escape}} {% endfor %} """ } @@ -1140,11 +1144,16 @@ def create_missing_domain(self, name): return super().create_missing_domain(name) if name == "sync" and self.default_clk is not None: - clk_i = self.request(self.default_clk).i + m = Module() + + clk_io = self.request(self.default_clk, dir="-") + m.submodules.clk_buf = clk_buf = io.Buffer("i", clk_io) + clk_i = clk_buf.i if self.default_rst is not None: - rst_i = self.request(self.default_rst).i + rst_io = self.request(self.default_rst, dir="-") + m.submodules.rst_buf = rst_buf = io.Buffer("i", rst_io) + rst_i = rst_buf.i - m = Module() ready = Signal() m.submodules += Instance(STARTUP_PRIMITIVE[self.family], o_EOS=ready) m.domains += ClockDomain("sync", reset_less=self.default_rst is None) @@ -1173,7 +1182,8 @@ def create_missing_domain(self, name): def add_clock_constraint(self, clock, frequency): super().add_clock_constraint(clock, frequency) - clock.attrs["keep"] = "TRUE" + if not isinstance(clock, IOPort): + clock.attrs["keep"] = "TRUE" def get_io_buffer(self, buffer): if isinstance(buffer, io.Buffer): @@ -1230,13 +1240,13 @@ def get_ff_sync(self, ff_sync): for index in range(ff_sync._stages)] if self.toolchain == "Vivado": if ff_sync._max_input_delay is None: - flops[0].attrs["amaranth.vivado.false_path"] = "TRUE" + Value.cast(flops[0]).attrs["amaranth.vivado.false_path"] = "TRUE" else: - flops[0].attrs["amaranth.vivado.max_delay"] = str(ff_sync._max_input_delay * 1e9) + Value.cast(flops[0]).attrs["amaranth.vivado.max_delay"] = str(ff_sync._max_input_delay * 1e9) elif ff_sync._max_input_delay is not None: raise NotImplementedError("Platform '{}' does not support constraining input delay " "for FFSynchronizer" - .format(type(self).__name__)) + .format(type(self).__qualname__)) for i, o in zip((ff_sync.i, *flops), flops): m.d[ff_sync._o_domain] += o.eq(i) m.d.comb += ff_sync.o.eq(flops[-1]) @@ -1255,7 +1265,7 @@ def get_async_ff_sync(self, async_ff_sync): flops_d = Signal(async_ff_sync._stages, reset_less=True) flops_pre = Signal(reset_less=True) for i in range(async_ff_sync._stages): - flop = Instance("FDPE", p_INIT=1, o_Q=flops_q[i], + flop = Instance("FDPE", p_INIT=Const(1), o_Q=flops_q[i], i_C=ClockSignal(async_ff_sync._o_domain), i_CE=Const(1), i_PRE=flops_pre, i_D=flops_d[i], a_ASYNC_REG="TRUE") @@ -1284,7 +1294,7 @@ def get_async_ff_sync(self, async_ff_sync): elif async_ff_sync._max_input_delay is not None: raise NotImplementedError("Platform '{}' does not support constraining input delay " "for AsyncFFSynchronizer" - .format(type(self).__name__)) + .format(type(self).__qualname__)) for i, o in zip((0, *flops_q), flops_d): m.d.comb += o.eq(i) diff --git a/docs/changes.rst b/docs/changes.rst index 028091636..d2414be52 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -1,7 +1,7 @@ Changelog ######### -This document describes changes to the public interfaces in the Amaranth language and standard library. It does not include most bug fixes or implementation changes. +This document describes changes to the public interfaces in the Amaranth language and standard library. It does not include most bug fixes or implementation changes; versions which do not include notable changes are not listed here. Documentation for past releases @@ -9,6 +9,11 @@ Documentation for past releases Documentation for past releases of the Amaranth language and toolchain is available online: +* `Amaranth 0.5.4 <https://amaranth-lang.org/docs/amaranth/v0.5.3/>`_ +* `Amaranth 0.5.3 <https://amaranth-lang.org/docs/amaranth/v0.5.3/>`_ +* `Amaranth 0.5.2 <https://amaranth-lang.org/docs/amaranth/v0.5.2/>`_ +* `Amaranth 0.5.1 <https://amaranth-lang.org/docs/amaranth/v0.5.1/>`_ +* `Amaranth 0.5.0 <https://amaranth-lang.org/docs/amaranth/v0.5.0/>`_ * `Amaranth 0.4.5 <https://amaranth-lang.org/docs/amaranth/v0.4.5/>`_ * `Amaranth 0.4.4 <https://amaranth-lang.org/docs/amaranth/v0.4.4/>`_ * `Amaranth 0.4.3 <https://amaranth-lang.org/docs/amaranth/v0.4.3/>`_ @@ -18,8 +23,75 @@ Documentation for past releases of the Amaranth language and toolchain is availa * `Amaranth 0.3 <https://amaranth-lang.org/docs/amaranth/v0.3/>`_ -Version 0.5 -=========== +Version 0.5.4 +============= + +Updated to address deprecations in Yosys 0.48. + + +Version 0.5.3 +============= + + +Language changes +---------------- + +* Added: individual bits of the same signal can now be assigned from different modules or domains. + + +Toolchain changes +----------------- + +* Added: the Amaranth RPC server can now elaborate :class:`amaranth.lib.wiring.Component` objects on demand. + + +Version 0.5.2 +============= + + +Standard library changes +------------------------ + +.. currentmodule:: amaranth.lib + +* Added: constants of :class:`amaranth.lib.data.ArrayLayout` can be indexed with negative integers or slices. +* Added: :py:`len()` works on constants of :class:`amaranth.lib.data.ArrayLayout`. +* Added: constants of :class:`amaranth.lib.data.ArrayLayout` are iterable. + + +Platform integration changes +---------------------------- + +.. currentmodule:: amaranth.vendor + +* Added: :meth:`Platform.request` accepts :py:`dir="-"` for resources with subsignals. + + +Version 0.5.1 +============= + + +Implemented RFCs +---------------- + +.. _RFC 69: https://amaranth-lang.org/rfcs/0069-simulation-port.html + +* `RFC 69`_: Add a ``lib.io.PortLike`` object usable in simulation + + +Standard library changes +------------------------ + +.. currentmodule:: amaranth.lib + +* Added: views of :class:`amaranth.lib.data.ArrayLayout` can be indexed with negative integers or slices. +* Added: :py:`len()` works on views of :class:`amaranth.lib.data.ArrayLayout`. +* Added: views of :class:`amaranth.lib.data.ArrayLayout` are iterable. +* Added: :class:`io.SimulationPort`. (`RFC 69`_) + + +Version 0.5.0 +============= The Migen compatibility layer has been removed. @@ -46,6 +118,7 @@ Apply the following changes to code written against Amaranth 0.4 to migrate it t * Update uses of :meth:`Simulator.run_until <amaranth.sim.Simulator.run_until>` to remove the :py:`run_passive=True` argument. If the code uses :py:`run_passive=False`, ensure it still works with the new behavior. * Update uses of :py:`amaranth.utils.log2_int(need_pow2=False)` to :func:`amaranth.utils.ceil_log2`. * Update uses of :py:`amaranth.utils.log2_int(need_pow2=True)` to :func:`amaranth.utils.exact_log2`. +* Replace uses of :py:`a.implies(b)` with `~a | b`. Implemented RFCs @@ -116,9 +189,10 @@ Language changes * Deprecated: :func:`amaranth.utils.log2_int`. (`RFC 17`_) * Deprecated: :class:`amaranth.hdl.Memory`. (`RFC 45`_) * Deprecated: upwards propagation of clock domains. (`RFC 59`_) -* Removed: (deprecated in 0.4) :meth:`Const.normalize`. (`RFC 5`_) -* Removed: (deprecated in 0.4) :class:`Repl`. (`RFC 10`_) -* Removed: (deprecated in 0.4) :class:`ast.Sample`, :class:`ast.Past`, :class:`ast.Stable`, :class:`ast.Rose`, :class:`ast.Fell`. +* Deprecated: :meth:`Value.implies`. +* Removed: (deprecated in 0.4.0) :meth:`Const.normalize`. (`RFC 5`_) +* Removed: (deprecated in 0.4.0) :class:`Repl`. (`RFC 10`_) +* Removed: (deprecated in 0.4.0) :class:`ast.Sample`, :class:`ast.Past`, :class:`ast.Stable`, :class:`ast.Rose`, :class:`ast.Fell`. * Removed: assertion names in :class:`Assert`, :class:`Assume` and :class:`Cover`. (`RFC 50`_) * Removed: accepting non-subclasses of :class:`Elaboratable` as elaboratables. @@ -137,9 +211,9 @@ Standard library changes * Added: :mod:`amaranth.lib.meta`, :class:`amaranth.lib.wiring.ComponentMetadata`. (`RFC 30`_) * Added: :mod:`amaranth.lib.stream`. (`RFC 61`_) * Deprecated: :mod:`amaranth.lib.coding`. (`RFC 63`_) -* Removed: (deprecated in 0.4) :mod:`amaranth.lib.scheduler`. (`RFC 19`_) -* Removed: (deprecated in 0.4) :class:`amaranth.lib.fifo.FIFOInterface` with :py:`fwft=False`. (`RFC 20`_) -* Removed: (deprecated in 0.4) :class:`amaranth.lib.fifo.SyncFIFO` with :py:`fwft=False`. (`RFC 20`_) +* Removed: (deprecated in 0.4.0) :mod:`amaranth.lib.scheduler`. (`RFC 19`_) +* Removed: (deprecated in 0.4.0) :class:`amaranth.lib.fifo.FIFOInterface` with :py:`fwft=False`. (`RFC 20`_) +* Removed: (deprecated in 0.4.0) :class:`amaranth.lib.fifo.SyncFIFO` with :py:`fwft=False`. (`RFC 20`_) Toolchain changes @@ -152,8 +226,9 @@ Toolchain changes * Changed: :meth:`Simulator.run_until <amaranth.sim.Simulator.run_until>` always runs the simulation until the given deadline, even when no critical processes or testbenches are present. * Deprecated: :py:`Settle` simulation command. (`RFC 27`_) * Deprecated: :py:`Simulator.add_sync_process`. (`RFC 27`_) +* Deprecated: generator-based simulation processes and testbenches. (`RFC 36`_) * Deprecated: the :py:`run_passive` argument to :meth:`Simulator.run_until <amaranth.sim.Simulator.run_until>` has been deprecated, and does nothing. -* Removed: (deprecated in 0.4) use of mixed-case toolchain environment variable names, such as ``NMIGEN_ENV_Diamond`` or ``AMARANTH_ENV_Diamond``; use upper-case environment variable names, such as ``AMARANTH_ENV_DIAMOND``. +* Removed: (deprecated in 0.4.0) use of mixed-case toolchain environment variable names, such as ``NMIGEN_ENV_Diamond`` or ``AMARANTH_ENV_Diamond``; use upper-case environment variable names, such as ``AMARANTH_ENV_DIAMOND``. Platform integration changes @@ -166,11 +241,11 @@ Platform integration changes * Added: ``build.sh`` begins with ``#!/bin/sh``. * Changed: ``IntelPlatform`` renamed to ``AlteraPlatform``. * Deprecated: argument :py:`run_script=` in :meth:`BuildPlan.execute_local`. -* Removed: (deprecated in 0.4) :mod:`vendor.intel`, :mod:`vendor.lattice_ecp5`, :mod:`vendor.lattice_ice40`, :mod:`vendor.lattice_machxo2_3l`, :mod:`vendor.quicklogic`, :mod:`vendor.xilinx`. (`RFC 18`_) +* Removed: (deprecated in 0.4.0) :mod:`vendor.intel`, :mod:`vendor.lattice_ecp5`, :mod:`vendor.lattice_ice40`, :mod:`vendor.lattice_machxo2_3l`, :mod:`vendor.quicklogic`, :mod:`vendor.xilinx`. (`RFC 18`_) -Version 0.4 -=========== +Version 0.4.0 +============= Support has been added for a new and improved way of defining data structures in :mod:`amaranth.lib.data` and component interfaces in :mod:`amaranth.lib.wiring`, as defined in `RFC 1`_ and `RFC 2`_. :class:`Record` has been deprecated. In a departure from the usual policy, to give designers additional time to migrate, :class:`Record` will be removed in Amaranth 0.6 (one release later than normal). diff --git a/docs/guide.rst b/docs/guide.rst index 71838a96c..4923f0cfe 100644 --- a/docs/guide.rst +++ b/docs/guide.rst @@ -170,6 +170,14 @@ Specifying a shape with a range is convenient for counters, indexes, and all oth Python ranges are *exclusive* or *half-open*, meaning they do not contain their ``.stop`` element. Because of this, values with shapes cast from a ``range(stop)`` where ``stop`` is a power of 2 are not wide enough to represent ``stop`` itself: + .. doctest:: + :hide: + + >>> import warnings + >>> _warning_filters_backup = warnings.catch_warnings() + >>> _warning_filters_backup.__enter__() # have to do this horrific hack to make it work with `PYTHONWARNINGS=error` :( + >>> warnings.simplefilter("default", amaranth.hdl._ast.SyntaxWarning) + .. doctest:: >>> fencepost = C(256, range(256)) @@ -180,6 +188,11 @@ Specifying a shape with a range is convenient for counters, indexes, and all oth >>> fencepost.value 0 + .. doctest:: + :hide: + + >>> _warning_filters_backup.__exit__() + Amaranth detects uses of :class:`Const` and :class:`Signal` that invoke such an off-by-one error, and emits a diagnostic message. .. note:: @@ -670,6 +683,14 @@ Python expression Amaranth expression (boolean operands) When applied to Amaranth boolean values, the ``~`` operator computes negation, and when applied to Python boolean values, the ``not`` operator also computes negation. However, the ``~`` operator applied to Python boolean values produces an unexpected result: + .. doctest:: + :hide: + + >>> import warnings + >>> _warning_filters_backup = warnings.catch_warnings() + >>> _warning_filters_backup.__enter__() # have to do this horrific hack to make it work with `PYTHONWARNINGS=error` :( + >>> warnings.simplefilter("ignore", DeprecationWarning) + .. doctest:: >>> ~False @@ -688,6 +709,11 @@ Python expression Amaranth expression (boolean operands) >>> ~use_stb | stb # WRONG! MSB of 2-bit wide OR expression is always 1 (| (const 2'sd-2) (sig stb)) + .. doctest:: + :hide: + + >>> _warning_filters_backup.__exit__() + Amaranth automatically detects some cases of misuse of ``~`` and emits a detailed diagnostic message. .. TODO: this isn't quite reliable, #380 @@ -914,7 +940,7 @@ If the name of a domain is not known upfront, the ``m.d["<domain>"] += ...`` syn .. _lang-signalgranularity: -Every signal included in the target of an assignment becomes a part of the domain, or equivalently, *driven* by that domain. A signal can be either undriven or driven by exactly one domain; it is an error to add two assignments to the same signal to two different domains: +Every signal bit included in the target of an assignment becomes a part of the domain, or equivalently, *driven* by that domain. A signal bit can be either undriven or driven by exactly one domain; it is an error to add two assignments to the same signal bit to two different domains: .. doctest:: @@ -923,19 +949,15 @@ Every signal included in the target of an assignment becomes a part of the domai >>> m.d.sync += d.eq(0) Traceback (most recent call last): ... - amaranth.hdl.dsl.SyntaxError: Driver-driver conflict: trying to drive (sig d) from d.sync, but it is already driven from d.comb + amaranth.hdl.dsl.SyntaxError: Driver-driver conflict: trying to drive (sig d) bit 0 from d.sync, but it is already driven from d.comb -.. note:: - - Clearly, Amaranth code that drives a single bit of a signal from two different domains does not describe a meaningful circuit. However, driving two different bits of a signal from two different domains does not inherently cause such a conflict. Would Amaranth accept the following code? +However, two different bits of a signal can be driven from two different domains without an issue: - .. code-block:: - - e = Signal(2) - m.d.comb += e[0].eq(0) - m.d.sync += e[1].eq(1) +.. testcode:: - The answer is no. While this kind of code is occasionally useful, rejecting it greatly simplifies backends, simulators, and analyzers. + e = Signal(2) + m.d.comb += e[0].eq(1) + m.d.sync += e[1].eq(0) In addition to assignments, :ref:`assertions <lang-assert>` and :ref:`debug prints <lang-print>` can be added using the same syntax. diff --git a/docs/stdlib/io.rst b/docs/stdlib/io.rst index 3740ccfdd..295fbc867 100644 --- a/docs/stdlib/io.rst +++ b/docs/stdlib/io.rst @@ -60,7 +60,8 @@ All of the following examples assume that one of the built-in FPGA platforms is .. testcode:: - from amaranth.lib import io, wiring + from amaranth.sim import Simulator + from amaranth.lib import io, wiring, stream from amaranth.lib.wiring import In, Out @@ -191,6 +192,74 @@ In this example of a `source-synchronous interface <https://en.wikipedia.org/wik This component transmits :py:`dout` on each cycle as two halves: the low 8 bits on the rising edge of the data clock, and the high 8 bits on the falling edge of the data clock. The transmission is *edge-aligned*, meaning that the data edges exactly coincide with the clock edges. +Simulation +---------- + +The Amaranth simulator, :mod:`amaranth.sim`, cannot simulate :ref:`core I/O values <lang-iovalues>` or :ref:`I/O buffer instances <lang-iobufferinstance>` as it only operates on unidirectionally driven two-state wires. This module provides a simulation-only library I/O port, :class:`SimulationPort`, so that components that use library I/O buffers can be tested. + +A component that is designed for testing should accept the library I/O ports it will drive as constructor parameters rather than requesting them from the platform directly. Synthesizable designs will instantiate the component with a :class:`SingleEndedPort`, :class:`DifferentialPort`, or a platform-specific library I/O port, while tests will instantiate the component with a :class:`SimulationPort`. Tests are able to inject inputs into the component using :py:`sim_port.i`, capture the outputs of the component via :py:`sim_port.o`, and ensure that the component is driving the outputs at the appropriate times using :py:`sim_port.oe`. + +For example, consider a simple serializer that accepts a stream of multi-bit data words and outputs them bit by bit. It can be tested as follows: + +.. testcode:: + + class OutputSerializer(wiring.Component): + data: In(stream.Signature(8)) + + def __init__(self, dclk_port, dout_port): + self.dclk_port = dclk_port + self.dout_port = dout_port + + super().__init__() + + def elaborate(self, platform): + m = Module() + + m.submodules.dclk = dclk = io.Buffer("o", self.dclk_port) + m.submodules.dout = dout = io.Buffer("o", self.dout_port) + + index = Signal(range(8)) + m.d.comb += dout.o.eq(self.data.payload.bit_select(index, 1)) + + with m.If(self.data.valid): + m.d.sync += dclk.o.eq(~dclk.o) + with m.If(dclk.o): + m.d.sync += index.eq(index + 1) + with m.If(index == 7): + m.d.comb += self.data.ready.eq(1) + + return m + + def test_output_serializer(): + dclk_port = io.SimulationPort("o", 1) + dout_port = io.SimulationPort("o", 1) + + dut = OutputSerializer(dclk_port, dout_port) + + async def testbench_write_data(ctx): + ctx.set(dut.data.payload, 0xA1) + ctx.set(dut.data.valid, 1) + await ctx.tick().until(dut.data.ready) + ctx.set(dut.data.valid, 0) + + async def testbench_sample_output(ctx): + for bit in [1,0,0,0,0,1,0,1]: + _, dout_value = await ctx.posedge(dut.dclk_port.o).sample(dut.dout_port.o) + assert ctx.get(dut.dout_port.oe) == 1, "DUT is not driving the data output" + assert dout_value == bit, "DUT drives the wrong value on data output" + + sim = Simulator(dut) + sim.add_clock(1e-6) + sim.add_testbench(testbench_write_data) + sim.add_testbench(testbench_sample_output) + sim.run() + +.. testcode:: + :hide: + + test_output_serializer() + + Ports ----- @@ -199,6 +268,7 @@ Ports .. autoclass:: PortLike .. autoclass:: SingleEndedPort .. autoclass:: DifferentialPort +.. autoclass:: SimulationPort Buffers diff --git a/pyproject.toml b/pyproject.toml index 1d74683c6..37abedab1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -75,13 +75,14 @@ test = [ ] docs = [ "sphinx~=7.1", - "sphinxcontrib-platformpicker~=1.3", - "sphinxcontrib-yowasp-wavedrom==1.7", # exact version to avoid changes in rendering + "sphinxcontrib-platformpicker~=1.4", + "sphinxcontrib-yowasp-wavedrom==1.8", # exact version to avoid changes in rendering "sphinx-rtd-theme~=2.0", "sphinx-autobuild", ] examples = [ - "amaranth-boards @ git+https://github.com/amaranth-lang/amaranth-boards.git" + # pin to last commit with py3.8 support + "amaranth-boards @ git+https://github.com/amaranth-lang/amaranth-boards.git@19b97324ecf9111c5d16377af79f82aad761c476" ] [tool.pdm.scripts] diff --git a/tests/test_back_rtlil.py b/tests/test_back_rtlil.py index 6c4dbcc81..eefc39911 100644 --- a/tests/test_back_rtlil.py +++ b/tests/test_back_rtlil.py @@ -1214,7 +1214,6 @@ def test_sync(self): wire width 8 output 3 \o wire width 8 $1 process $2 - assign $1 [7:0] \o [7:0] assign $1 [7:0] \i [7:0] switch \rst [0] case 1'1 @@ -2009,6 +2008,51 @@ def test_print_align(self): end """) + def test_escape_curly(self): + m = Module() + m.d.comb += [ + Print("{"), + Print("}"), + ] + self.assertRTLIL(m, [], R""" + attribute \generator "Amaranth" + attribute \top 1 + module \top + wire width 1 $1 + wire width 1 $2 + process $3 + assign $1 [0] 1'0 + assign $1 [0] 1'1 + end + cell $print $4 + parameter \FORMAT "{{\n" + parameter \ARGS_WIDTH 0 + parameter signed \PRIORITY 32'11111111111111111111111111111110 + parameter \TRG_ENABLE 0 + parameter \TRG_WIDTH 0 + parameter \TRG_POLARITY 0 + connect \EN $1 [0] + connect \ARGS { } + connect \TRG { } + end + process $5 + assign $2 [0] 1'0 + assign $2 [0] 1'1 + end + cell $print $6 + parameter \FORMAT "}}\n" + parameter \ARGS_WIDTH 0 + parameter signed \PRIORITY 32'11111111111111111111111111111100 + parameter \TRG_ENABLE 0 + parameter \TRG_WIDTH 0 + parameter \TRG_POLARITY 0 + connect \EN $2 [0] + connect \ARGS { } + connect \TRG { } + end + end + """) + class DetailTestCase(RTLILTestCase): def test_enum(self): @@ -2024,7 +2068,7 @@ class MyEnum(enum.Enum, shape=unsigned(2)): attribute \generator "Amaranth" attribute \top 1 module \top - attribute \enum_base_type "MyEnum" + attribute \enum_base_type "DetailTestCase.test_enum.<locals>.MyEnum" attribute \enum_value_00 "A" attribute \enum_value_01 "B" attribute \enum_value_10 "C" @@ -2052,7 +2096,7 @@ class Meow(data.Struct): attribute \top 1 module \top wire width 13 input 0 \sig - attribute \enum_base_type "MyEnum" + attribute \enum_base_type "DetailTestCase.test_struct.<locals>.MyEnum" attribute \enum_value_00 "A" attribute \enum_value_01 "B" attribute \enum_value_10 "C" diff --git a/tests/test_build_res.py b/tests/test_build_res.py index bda61b480..758cdce63 100644 --- a/tests/test_build_res.py +++ b/tests/test_build_res.py @@ -84,6 +84,12 @@ def test_request_with_dir(self): scl_buffer._MustUse__silence = True sda_buffer._MustUse__silence = True + def test_request_subsignal_dash(self): + with _ignore_deprecated(): + i2c = self.cm.request("i2c", 0, dir="-") + self.assertIsInstance(i2c.sda, SingleEndedPort) + self.assertIsInstance(i2c.scl, SingleEndedPort) + def test_request_tristate(self): with _ignore_deprecated(): i2c = self.cm.request("i2c", 0) @@ -226,17 +232,17 @@ def test_request_clock(self): ) = self.cm.iter_pins() clk100_buffer._MustUse__silence = True clk50_buffer._MustUse__silence = True - self.assertEqual(list(self.cm.iter_clock_constraints()), [ - (clk100.i, clk100_port.p, 100e6), - (clk50.i, clk50_port.io, 50e6) + self.assertEqual(list(self.cm.iter_port_clock_constraints()), [ + (clk100_port.p, 100e6), + (clk50_port.io, 50e6) ]) def test_add_clock(self): with _ignore_deprecated(): i2c = self.cm.request("i2c") self.cm.add_clock_constraint(i2c.scl.o, 100e3) - self.assertEqual(list(self.cm.iter_clock_constraints()), [ - (i2c.scl.o, None, 100e3) + self.assertEqual(list(self.cm.iter_signal_clock_constraints()), [ + (i2c.scl.o, 100e3) ]) ((_, _, scl_buffer), (_, _, sda_buffer)) = self.cm.iter_pins() scl_buffer._MustUse__silence = True @@ -269,7 +275,7 @@ def test_wrong_lookup(self): def test_wrong_clock_signal(self): with self.assertRaisesRegex(TypeError, - r"^Object None is not a Signal$"): + r"^Object None is not a Signal or IOPort$"): self.cm.add_clock_constraint(None, 10e6) def test_wrong_clock_frequency(self): @@ -335,10 +341,8 @@ def test_wrong_request_with_xdr_dict(self): def test_wrong_clock_constraint_twice(self): with _ignore_deprecated(): - clk100 = self.cm.request("clk100") - (pin, port, buffer), = self.cm.iter_pins() - buffer._MustUse__silence = True + clk100 = self.cm.request("clk100", dir="-") with self.assertRaisesRegex(ValueError, - (r"^Cannot add clock constraint on \(sig clk100_0__i\), which is already " + (r"^Cannot add clock constraint on \(io-port clk100_0__p\), which is already " r"constrained to 100000000\.0 Hz$")): - self.cm.add_clock_constraint(clk100.i, 1e6) + self.cm.add_clock_constraint(clk100.p, 1e6) diff --git a/tests/test_hdl_ast.py b/tests/test_hdl_ast.py index 666968fe6..f4ae0065b 100644 --- a/tests/test_hdl_ast.py +++ b/tests/test_hdl_ast.py @@ -185,7 +185,7 @@ def from_bits(self, bits): class ShapeCastableTestCase(FHDLTestCase): def test_no_override(self): with self.assertRaisesRegex(TypeError, - r"^Class 'MockShapeCastableNoOverride' deriving from 'ShapeCastable' must " + r"^Class '.+\.MockShapeCastableNoOverride' deriving from 'ShapeCastable' must " r"override the 'as_shape' method$"): class MockShapeCastableNoOverride(ShapeCastable): def __init__(self): @@ -213,7 +213,7 @@ def test_abstract(self): def test_no_from_bits(self): with self.assertWarnsRegex(DeprecationWarning, - r"^Class 'MockShapeCastableNoFromBits' deriving from 'ShapeCastable' does " + r"^Class '.+\.MockShapeCastableNoFromBits' deriving from 'ShapeCastable' does " r"not override the 'from_bits' method, which will be required in Amaranth 0.6$"): class MockShapeCastableNoFromBits(ShapeCastable): def __init__(self, dest): @@ -521,6 +521,20 @@ def test_hash(self): with self.assertRaises(TypeError): hash(Const(0)) + def test_enum(self): + e1 = Const(UnsignedEnum.FOO) + self.assertIsInstance(e1, Const) + self.assertEqual(e1.shape(), unsigned(2)) + e2 = Const(SignedEnum.FOO) + self.assertIsInstance(e2, Const) + self.assertEqual(e2.shape(), signed(2)) + e3 = Const(TypedEnum.FOO) + self.assertIsInstance(e3, Const) + self.assertEqual(e3.shape(), unsigned(2)) + e4 = Const(UnsignedEnum.FOO, 4) + self.assertIsInstance(e4, Const) + self.assertEqual(e4.shape(), unsigned(4)) + def test_shape_castable(self): class MockConstValue(ValueCastable): def __init__(self, value): @@ -528,7 +542,7 @@ def __init__(self, value): def shape(self): return MockConstShape() - + def as_value(self): return Const(self.value, 8) @@ -1250,6 +1264,14 @@ def test_init_wrong_too_wide(self): r"^Initial value -2 will be truncated to the signal shape signed\(1\)$"): Signal(signed(1), init=-2) + def test_init_truncated(self): + s1 = Signal(unsigned(2), init=-1) + self.assertEqual(s1.init, 0b11) + with warnings.catch_warnings(): + warnings.filterwarnings(action="ignore", category=SyntaxWarning) + s2 = Signal(signed(2), init=-33) + self.assertEqual(s2.init, -1) + def test_init_wrong_fencepost(self): with self.assertRaisesRegex(SyntaxError, r"^Initial value 10 equals the non-inclusive end of the signal shape " @@ -1327,7 +1349,7 @@ class Color(Enum): BLUE = 2 s = Signal(decoder=Color) self.assertEqual(s.decoder, Color) - self.assertRepr(s._format, "(format-enum (sig s) 'Color' (1 'RED') (2 'BLUE'))") + self.assertRepr(s._format, f"(format-enum (sig s) '{Color.__qualname__}' (1 'RED') (2 'BLUE'))") def test_enum(self): s1 = Signal(UnsignedEnum) @@ -1515,14 +1537,14 @@ def __getattr__(self, attr): class ValueCastableTestCase(FHDLTestCase): def test_no_override(self): with self.assertRaisesRegex(TypeError, - r"^Class 'MockValueCastableNoOverrideAsValue' deriving from 'ValueCastable' must " + r"^Class '.+\.MockValueCastableNoOverrideAsValue' deriving from 'ValueCastable' must " r"override the 'as_value' method$"): class MockValueCastableNoOverrideAsValue(ValueCastable): def __init__(self): pass with self.assertRaisesRegex(TypeError, - r"^Class 'MockValueCastableNoOverrideShapec' deriving from 'ValueCastable' must " + r"^Class '.+\.MockValueCastableNoOverrideShapec' deriving from 'ValueCastable' must " r"override the 'shape' method$"): class MockValueCastableNoOverrideShapec(ValueCastable): def __init__(self): diff --git a/tests/test_hdl_dsl.py b/tests/test_hdl_dsl.py index ecbed9d04..0ab6b48b5 100644 --- a/tests/test_hdl_dsl.py +++ b/tests/test_hdl_dsl.py @@ -34,7 +34,6 @@ def test_d_comb(self): m = Module() m.d.comb += self.c1.eq(1) m._flush() - self.assertEqual(m._driving[self.c1], "comb") self.assertRepr(m._statements["comb"], """( (eq (sig c1) (const 1'd1)) )""") @@ -43,7 +42,6 @@ def test_d_sync(self): m = Module() m.d.sync += self.c1.eq(1) m._flush() - self.assertEqual(m._driving[self.c1], "sync") self.assertRepr(m._statements["sync"], """( (eq (sig c1) (const 1'd1)) )""") @@ -52,7 +50,6 @@ def test_d_pix(self): m = Module() m.d.pix += self.c1.eq(1) m._flush() - self.assertEqual(m._driving[self.c1], "pix") self.assertRepr(m._statements["pix"], """( (eq (sig c1) (const 1'd1)) )""") @@ -61,7 +58,6 @@ def test_d_index(self): m = Module() m.d["pix"] += self.c1.eq(1) m._flush() - self.assertEqual(m._driving[self.c1], "pix") self.assertRepr(m._statements["pix"], """( (eq (sig c1) (const 1'd1)) )""") @@ -74,7 +70,7 @@ def test_d_no_conflict(self): def test_d_conflict(self): m = Module() with self.assertRaisesRegex(SyntaxError, - (r"^Driver-driver conflict: trying to drive \(sig c1\) from d\.sync, but it " + (r"^Driver-driver conflict: trying to drive \(sig c1\) bit 0 from d\.sync, but it " r"is already driven from d\.comb$")): m.d.comb += self.c1.eq(1) m.d.sync += self.c1.eq(1) diff --git a/tests/test_hdl_ir.py b/tests/test_hdl_ir.py index 5b6a682cd..4899d188b 100644 --- a/tests/test_hdl_ir.py +++ b/tests/test_hdl_ir.py @@ -265,7 +265,7 @@ def test_port_domain(self): (cell 1 0 (+ (cat 5.0:4 1'd0) 5'd1)) (cell 2 0 (matches 0.3 1)) (cell 3 0 (priority_match 1 2.0)) - (cell 4 0 (assignment_list 5.0:4 (1 0:4 1.0:4) (3.0 0:4 4'd0))) + (cell 4 0 (assignment_list 1.0:4 (3.0 0:4 4'd0))) (cell 5 0 (flipflop 4.0:4 0 pos 0.2 0)) ) """) @@ -282,7 +282,7 @@ def test_port_autodomain(self): (cell 1 0 (+ (cat 5.0:4 1'd0) 5'd1)) (cell 2 0 (matches 0.3 1)) (cell 3 0 (priority_match 1 2.0)) - (cell 4 0 (assignment_list 5.0:4 (1 0:4 1.0:4) (3.0 0:4 4'd0))) + (cell 4 0 (assignment_list 1.0:4 (3.0 0:4 4'd0))) (cell 5 0 (flipflop 4.0:4 0 pos 0.2 0)) ) """) @@ -3411,6 +3411,121 @@ def test_assert(self): """) +class SplitDriverTestCase(FHDLTestCase): + def test_split_domain(self): + m = Module() + o = Signal(10, init=0x123) + i1 = Signal(2) + i2 = Signal(2) + i3 = Signal(2) + i4 = Signal(2) + cond = Signal() + m.domains.a = ClockDomain() + m.domains.b = ClockDomain() + m.d.a += o[:2].eq(i1) + m.d.b += o[2:4].eq(i2) + with m.If(cond): + m.d.a += o[4:6].eq(i3) + m.d.comb += o[8:10].eq(i4) + nl = build_netlist(Fragment.get(m, None), [ + o, i1, i2, i3, i4, cond, + ClockSignal("a"), ResetSignal("a"), + ClockSignal("b"), ResetSignal("b"), + ]) + self.assertRepr(nl, """ + ( + (module 0 None ('top') + (input 'i1' 0.2:4) + (input 'i2' 0.4:6) + (input 'i3' 0.6:8) + (input 'i4' 0.8:10) + (input 'cond' 0.10) + (input 'a_clk' 0.11) + (input 'a_rst' 0.12) + (input 'b_clk' 0.13) + (input 'b_rst' 0.14) + (output 'o' (cat 6.0:2 12.0:2 8.0:2 2'd0 13.0:2)) + ) + (cell 0 0 (top + (input 'i1' 2:4) + (input 'i2' 4:6) + (input 'i3' 6:8) + (input 'i4' 8:10) + (input 'cond' 10:11) + (input 'a_clk' 11:12) + (input 'a_rst' 12:13) + (input 'b_clk' 13:14) + (input 'b_rst' 14:15) + (output 'o' (cat 6.0:2 12.0:2 8.0:2 2'd0 13.0:2)) + )) + (cell 1 0 (matches 0.10 1)) + (cell 2 0 (priority_match 1 1.0)) + (cell 3 0 (matches 0.12 1)) + (cell 4 0 (priority_match 1 3.0)) + (cell 5 0 (assignment_list 0.2:4 (4.0 0:2 2'd3))) + (cell 6 0 (flipflop 5.0:2 3 pos 0.11 0)) + (cell 7 0 (assignment_list 8.0:2 (2.0 0:2 0.6:8) (4.0 0:2 2'd2))) + (cell 8 0 (flipflop 7.0:2 2 pos 0.11 0)) + (cell 9 0 (matches 0.14 1)) + (cell 10 0 (priority_match 1 9.0)) + (cell 11 0 (assignment_list 0.4:6 (10.0 0:2 2'd0))) + (cell 12 0 (flipflop 11.0:2 0 pos 0.13 0)) + (cell 13 0 (assignment_list 2'd1 (2.0 0:2 0.8:10))) + ) + """) + + def test_split_module(self): + m = Module() + m.submodules.m1 = m1 = Module() + m.submodules.m2 = m2 = Module() + + i1 = Signal(4) + i2 = Signal(4) + i3 = Signal(2) + cond = Signal() + o = Signal(8) + m1.d.comb += o[:4].eq(i1) + m2.d.comb += o[4:].eq(i2) + with m2.If(cond): + m2.d.comb += o[5:7].eq(i3) + + nl = build_netlist(Fragment.get(m, None), [ + o, i1, i2, i3, cond, + ]) + self.assertRepr(nl, """ + ( + (module 0 None ('top') + (input 'i1' 0.2:6) + (input 'i2' 0.6:10) + (input 'i3' 0.10:12) + (input 'cond' 0.12) + (output 'o' (cat 0.2:6 3.0:4)) + ) + (module 1 0 ('top' 'm1') + (input 'i1' 0.2:6) + (input 'port$3$0' 3.0:4) + ) + (module 2 0 ('top' 'm2') + (input 'port$0$2' 0.2:6) + (input 'i2' 0.6:10) + (input 'i3' 0.10:12) + (input 'cond' 0.12) + (output 'port$3$0' 3.0:4) + ) + (cell 0 0 (top + (input 'i1' 2:6) + (input 'i2' 6:10) + (input 'i3' 10:12) + (input 'cond' 12:13) + (output 'o' (cat 0.2:6 3.0:4)) + )) + (cell 1 2 (matches 0.12 1)) + (cell 2 2 (priority_match 1 1.0)) + (cell 3 2 (assignment_list 0.6:10 (2.0 1:3 0.10:12))) + ) + """) + + class ConflictTestCase(FHDLTestCase): def test_domain_conflict(self): s = Signal() @@ -3420,7 +3535,7 @@ def test_domain_conflict(self): m1.d.comb += s.eq(2) m.submodules.m1 = m1 with self.assertRaisesRegex(DriverConflict, - r"^Signal \(sig s\) driven from domain comb at " + r"^Signal \(sig s\) bit 0 driven from domain comb at " r".*test_hdl_ir.py:\d+ and domain sync at " r".*test_hdl_ir.py:\d+$"): build_netlist(Fragment.get(m, None), []) @@ -3433,7 +3548,7 @@ def test_module_conflict(self): m1.d.sync += s.eq(2) m.submodules.m1 = m1 with self.assertRaisesRegex(DriverConflict, - r"^Signal \(sig s\) driven from module top\.m1 at " + r"^Signal \(sig s\) bit 0 driven from module top\.m1 at " r".*test_hdl_ir.py:\d+ and module top at " r".*test_hdl_ir.py:\d+$"): build_netlist(Fragment.get(m, None), []) @@ -3523,7 +3638,7 @@ class MyEnum(enum.Enum, shape=unsigned(2)): ]) self.assertEqual(nl.signal_fields[s1.as_value()], { (): SignalField(nl.signals[s1.as_value()], signed=False), - ('a',): SignalField(nl.signals[s1.as_value()][0:2], signed=False, enum_name="MyEnum", enum_variants={ + ('a',): SignalField(nl.signals[s1.as_value()][0:2], signed=False, enum_name=MyEnum.__qualname__, enum_variants={ 0: "A", 1: "B", 2: "C", @@ -3531,7 +3646,7 @@ class MyEnum(enum.Enum, shape=unsigned(2)): ('b',): SignalField(nl.signals[s1.as_value()][2:5], signed=True) }) self.assertEqual(nl.signal_fields[s2.as_value()], { - (): SignalField(nl.signals[s2.as_value()], signed=False, enum_name="MyEnum", enum_variants={ + (): SignalField(nl.signals[s2.as_value()], signed=False, enum_name=MyEnum.__qualname__, enum_variants={ 0: "A", 1: "B", 2: "C", @@ -3563,6 +3678,30 @@ def test_cycle(self): r"$"): build_netlist(Fragment.get(m, None), []) + def test_assignment_cycle(self): + a = Signal(2) + m = Module() + + with m.If(a[0]): + m.d.comb += a[0].eq(1) + + with self.assertRaisesRegex(CombinationalCycle, + r"^Combinational cycle detected, path:\n" + r".*test_hdl_ir.py:\d+: cell Matches bit 0\n" + r".*test_hdl_ir.py:\d+: signal a bit 0\n" + r".*test_hdl_ir.py:\d+: cell AssignmentList bit 0\n" + r".*test_hdl_ir.py:\d+: cell PriorityMatch bit 0\n" + r"$"): + build_netlist(Fragment.get(m, None), []) + + m = Module() + + with m.If(a[0]): + m.d.comb += a[1].eq(1) + + # no cycle here, a[1] gets assigned and a[0] gets checked + build_netlist(Fragment.get(m, None), []) + class DomainLookupTestCase(FHDLTestCase): def test_domain_lookup(self): @@ -3622,4 +3761,4 @@ def test_require_renamed(self): m.submodules += DomainRenamer("test")(RequirePosedge("sync")) with self.assertRaisesRegex(DomainRequirementFailed, r"^Domain test has a negedge clock, but posedge clock is required by top.U\$0 at .*$"): - Fragment.get(m, None).prepare() \ No newline at end of file + Fragment.get(m, None).prepare() diff --git a/tests/test_hdl_mem.py b/tests/test_hdl_mem.py index f165622de..722b52fd4 100644 --- a/tests/test_hdl_mem.py +++ b/tests/test_hdl_mem.py @@ -33,6 +33,33 @@ def test_row_elab(self): r"^Value \(memory-row \(memory-data data\) 0\) can only be used in simulator processes$"): m.d.comb += data[0].eq(1) + +class InitTestCase(FHDLTestCase): + def test_ones(self): + init = MemoryData.Init([-1, 12], shape=8, depth=2) + self.assertEqual(list(init), [0xff, 12]) + init = MemoryData.Init([-1, -12], shape=signed(8), depth=2) + self.assertEqual(list(init), [-1, -12]) + + def test_trunc(self): + with self.assertWarnsRegex(SyntaxWarning, + r"^Initial value -2 is signed, but the memory shape is unsigned\(8\)$"): + init = MemoryData.Init([-2, 12], shape=8, depth=2) + self.assertEqual(list(init), [0xfe, 12]) + with self.assertWarnsRegex(SyntaxWarning, + r"^Initial value 258 will be truncated to the memory shape unsigned\(8\)$"): + init = MemoryData.Init([258, 129], shape=8, depth=2) + self.assertEqual(list(init), [2, 129]) + with self.assertWarnsRegex(SyntaxWarning, + r"^Initial value 128 will be truncated to the memory shape signed\(8\)$"): + init = MemoryData.Init([128], shape=signed(8), depth=1) + self.assertEqual(list(init), [-128]) + with self.assertWarnsRegex(SyntaxWarning, + r"^Initial value -129 will be truncated to the memory shape signed\(8\)$"): + init = MemoryData.Init([-129], shape=signed(8), depth=1) + self.assertEqual(list(init), [127]) + + class MemoryTestCase(FHDLTestCase): def test_name(self): with _ignore_deprecated(): @@ -73,7 +100,7 @@ def test_init_wrong_type(self): with _ignore_deprecated(): with self.assertRaisesRegex(TypeError, (r"^Memory initialization value at address 1: " - r"'str' object cannot be interpreted as an integer$")): + r"Initial value must be a constant-castable expression, not '0'$")): m = Memory(width=8, depth=4, init=[1, "0"]) def test_attrs(self): diff --git a/tests/test_hdl_xfrm.py b/tests/test_hdl_xfrm.py index 0ab2eeeaa..389629655 100644 --- a/tests/test_hdl_xfrm.py +++ b/tests/test_hdl_xfrm.py @@ -227,6 +227,7 @@ def setUp(self): self.s1 = Signal() self.s2 = Signal(init=1) self.s3 = Signal(init=1, reset_less=True) + self.s4 = Signal(8, init=0x3a) self.c1 = Signal() def test_reset_default(self): @@ -281,6 +282,40 @@ def test_reset_value(self): ) """) + def test_reset_mask(self): + f = Fragment() + f.add_statements("sync", self.s4[2:4].eq(0)) + + f = ResetInserter(self.c1)(f) + self.assertRepr(f.statements["sync"], """ + ( + (eq (slice (sig s4) 2:4) (const 1'd0)) + (switch (sig c1) + (case 1 (eq (slice (sig s4) 2:4) (slice (const 8'd58) 2:4))) + ) + ) + """) + + f = Fragment() + f.add_statements("sync", self.s4[2:4].eq(0)) + f.add_statements("sync", self.s4[3:5].eq(0)) + f.add_statements("sync", self.s4[6:10].eq(0)) + + f = ResetInserter(self.c1)(f) + self.assertRepr(f.statements["sync"], """ + ( + (eq (slice (sig s4) 2:4) (const 1'd0)) + (eq (slice (sig s4) 3:5) (const 1'd0)) + (eq (slice (sig s4) 6:8) (const 1'd0)) + (switch (sig c1) + (case 1 + (eq (slice (sig s4) 2:5) (slice (const 8'd58) 2:5)) + (eq (slice (sig s4) 6:8) (slice (const 8'd58) 6:8)) + ) + ) + ) + """) + def test_reset_less(self): f = Fragment() f.add_statements("sync", self.s3.eq(0)) @@ -423,3 +458,31 @@ def test_composition(self): ) ) """) + +class LHSMaskCollectorTestCase(FHDLTestCase): + def test_slice(self): + s = Signal(8) + lhs = LHSMaskCollector() + lhs.visit_value(s[2:5], ~0) + self.assertEqual(lhs.lhs[s], 0x1c) + + def test_slice_slice(self): + s = Signal(8) + lhs = LHSMaskCollector() + lhs.visit_value(s[2:7][1:3], ~0) + self.assertEqual(lhs.lhs[s], 0x18) + + def test_slice_concat(self): + s1 = Signal(8) + s2 = Signal(8) + lhs = LHSMaskCollector() + lhs.visit_value(Cat(s1, s2)[4:11], ~0) + self.assertEqual(lhs.lhs[s1], 0xf0) + self.assertEqual(lhs.lhs[s2], 0x07) + + def test_slice_part(self): + s = Signal(8) + idx = Signal(8) + lhs = LHSMaskCollector() + lhs.visit_value(s.bit_select(idx, 5)[1:3], ~0) + self.assertEqual(lhs.lhs[s], 0xff) diff --git a/tests/test_lib_data.py b/tests/test_lib_data.py index 697f36650..bf2583eeb 100644 --- a/tests/test_lib_data.py +++ b/tests/test_lib_data.py @@ -477,6 +477,10 @@ def test_const_wrong(self): r"^Layout constant initializer must be a mapping or a sequence, not " r"<.+?object.+?>$"): sl.const(object()) + with self.assertRaisesRegex(ValueError, + r"^Layout constant initializer refers to key 'g', which is not a part " + r"of the layout$"): + sl.const({"g": 1}) sl2 = data.StructLayout({"f": unsigned(2)}) with self.assertRaisesRegex(ValueError, r"^Const layout StructLayout.* differs from shape layout StructLayout.*$"): @@ -610,10 +614,52 @@ def test_getitem(self): self.assertEqual(v["q"].shape(), signed(1)) self.assertRepr(v["r"][0], "(slice (slice (sig v) 0:4) 0:2)") self.assertRepr(v["r"][1], "(slice (slice (sig v) 0:4) 2:4)") + self.assertRepr(v["r"][-2], "(slice (slice (sig v) 0:4) 0:2)") + self.assertRepr(v["r"][-1], "(slice (slice (sig v) 0:4) 2:4)") self.assertRepr(v["r"][i], "(part (slice (sig v) 0:4) (sig i) 2 2)") self.assertRepr(v["t"][0]["u"], "(slice (slice (slice (sig v) 0:4) 0:2) 0:1)") self.assertRepr(v["t"][1]["v"], "(slice (slice (slice (sig v) 0:4) 2:4) 1:2)") + def test_getitem_slice(self): + a = Signal(data.ArrayLayout(unsigned(2), 5)) + self.assertEqual(a[1:3].shape(), data.ArrayLayout(unsigned(2), 2)) + self.assertRepr(a[1:3].as_value(), "(slice (sig a) 2:6)") + self.assertRepr(a[2:].as_value(), "(slice (sig a) 4:10)") + self.assertRepr(a[:-2].as_value(), "(slice (sig a) 0:6)") + self.assertRepr(a[-1:].as_value(), "(slice (sig a) 8:10)") + self.assertRepr(a[::-1].as_value(), """ + (cat + (slice (sig a) 8:10) + (slice (sig a) 6:8) + (slice (sig a) 4:6) + (slice (sig a) 2:4) + (slice (sig a) 0:2) + ) + """) + self.assertRepr(a[::2].as_value(), """ + (cat + (slice (sig a) 0:2) + (slice (sig a) 4:6) + (slice (sig a) 8:10) + ) + """) + self.assertRepr(a[1::2].as_value(), """ + (cat + (slice (sig a) 2:4) + (slice (sig a) 6:8) + ) + """) + + def test_array_iter(self): + a = Signal(data.ArrayLayout(unsigned(2), 5)) + l = list(a) + self.assertEqual(len(l), 5) + self.assertRepr(l[0], "(slice (sig a) 0:2)") + self.assertRepr(l[1], "(slice (sig a) 2:4)") + self.assertRepr(l[2], "(slice (sig a) 4:6)") + self.assertRepr(l[3], "(slice (sig a) 6:8)") + self.assertRepr(l[4], "(slice (sig a) 8:10)") + def test_getitem_custom_call(self): class Reverser(ShapeCastable): def as_shape(self): @@ -670,9 +716,17 @@ def test_index_wrong_struct_dynamic(self): r"with a value$"): Signal(data.StructLayout({}))[Signal(1)] + def test_index_wrong_oob(self): + with self.assertRaisesRegex(IndexError, + r"^Index 2 is out of range for array layout of length 2$"): + Signal(data.ArrayLayout(unsigned(2), 2))[2] + with self.assertRaisesRegex(IndexError, + r"^Index -3 is out of range for array layout of length 2$"): + Signal(data.ArrayLayout(unsigned(2), 2))[-3] + def test_index_wrong_slice(self): with self.assertRaisesRegex(TypeError, - r"^View cannot be indexed with a slice; did you mean to call `.as_value\(\)` " + r"^Non-array view cannot be indexed with a slice; did you mean to call `.as_value\(\)` " r"first\?$"): Signal(data.StructLayout({}))[0:1] @@ -740,7 +794,7 @@ def test_bug_837_array_layout_getattr(self): r"^View with an array layout does not have fields$"): Signal(data.ArrayLayout(unsigned(1), 1), init=[0]).init - def test_eq(self): + def test_compare(self): s1 = Signal(data.StructLayout({"a": unsigned(2)})) s2 = Signal(data.StructLayout({"a": unsigned(2)})) s3 = Signal(data.StructLayout({"a": unsigned(1), "b": unsigned(1)})) @@ -763,6 +817,14 @@ def test_eq(self): r"with the same layout, not .*$"): s1 != Const(0, 2) + def test_len(self): + s1 = Signal(data.StructLayout({"a": unsigned(2)})) + with self.assertRaisesRegex(TypeError, + r"^`len\(\)` can only be used on views of array layout, not StructLayout.*$"): + len(s1) + s2 = Signal(data.ArrayLayout(2, 3)) + self.assertEqual(len(s2), 3) + def test_operator(self): s1 = Signal(data.StructLayout({"a": unsigned(2)})) s2 = Signal(unsigned(2)) @@ -877,12 +939,28 @@ def test_getitem(self): self.assertEqual(v["q"], -1) self.assertEqual(v["r"][0], 3) self.assertEqual(v["r"][1], 2) + self.assertEqual(v["r"][-2], 3) + self.assertEqual(v["r"][-1], 2) self.assertRepr(v["r"][i], "(part (const 4'd11) (sig i) 2 2)") self.assertEqual(v["t"][0], data.Const(l, 2)) self.assertEqual(v["t"][1], data.Const(l, 2)) self.assertEqual(v["t"][0]["u"], 0) self.assertEqual(v["t"][1]["v"], 1) + def test_getitem_slice(self): + def A(n): + return data.ArrayLayout(unsigned(4), n) + v = data.Const(data.ArrayLayout(unsigned(4), 5), 0xabcde) + self.assertEqual(v[1:3], data.Const(A(2), 0xcd)) + self.assertEqual(v[2:], data.Const(A(3), 0xabc)) + self.assertEqual(v[:-2], data.Const(A(3), 0xcde)) + self.assertEqual(v[-1:], data.Const(A(1), 0xa)) + self.assertEqual(v[::-1], data.Const(A(5), 0xedcba)) + + def test_array_iter(self): + v = data.Const(data.ArrayLayout(unsigned(4), 5), 0xabcde) + self.assertEqual(list(v), [0xe, 0xd, 0xc, 0xb, 0xa]) + def test_getitem_custom_call(self): class Reverser(ShapeCastable): def as_shape(self): @@ -961,11 +1039,12 @@ def test_bug_837_array_layout_getattr(self): r"^Constant with an array layout does not have fields$"): data.Const(data.ArrayLayout(unsigned(1), 1), 0).init - def test_eq(self): + def test_compare(self): c1 = data.Const(data.StructLayout({"a": unsigned(2)}), 1) c2 = data.Const(data.StructLayout({"a": unsigned(2)}), 1) c3 = data.Const(data.StructLayout({"a": unsigned(2)}), 2) c4 = data.Const(data.StructLayout({"a": unsigned(1), "b": unsigned(1)}), 2) + c5 = data.Const(data.ArrayLayout(2, 4), 0b11100100) s1 = Signal(data.StructLayout({"a": unsigned(2)})) self.assertTrue(c1 == c2) self.assertFalse(c1 != c2) @@ -975,13 +1054,23 @@ def test_eq(self): self.assertRepr(c1 != s1, "(!= (const 2'd1) (sig s1))") self.assertRepr(s1 == c1, "(== (sig s1) (const 2'd1))") self.assertRepr(s1 != c1, "(!= (sig s1) (const 2'd1))") + self.assertTrue(c1 == {"a": 1}) + self.assertFalse(c1 == {"a": 2}) + self.assertFalse(c1 != {"a": 1}) + self.assertTrue(c1 != {"a": 2}) + self.assertTrue(c5 == [0,1,2,3]) + self.assertFalse(c5 == [0,1,3,3]) + self.assertFalse(c5 != [0,1,2,3]) + self.assertTrue(c5 != [0,1,3,3]) with self.assertRaisesRegex(TypeError, - r"^Constant with layout .* can only be compared to another view or constant with " - r"the same layout, not .*$"): + r"^Constant with layout .* can only be compared to another view, a constant " + r"with the same layout, or a dictionary or a list that can be converted to " + r"a constant with the same layout, not .*$"): c1 == c4 with self.assertRaisesRegex(TypeError, - r"^Constant with layout .* can only be compared to another view or constant with " - r"the same layout, not .*$"): + r"^Constant with layout .* can only be compared to another view, a constant " + r"with the same layout, or a dictionary or a list that can be converted to " + r"a constant with the same layout, not .*$"): c1 != c4 with self.assertRaisesRegex(TypeError, r"^View with layout .* can only be compared to another view or constant with " @@ -992,21 +1081,53 @@ def test_eq(self): r"the same layout, not .*$"): s1 != c4 with self.assertRaisesRegex(TypeError, - r"^Constant with layout .* can only be compared to another view or constant with " - r"the same layout, not .*$"): + r"^Constant with layout .* can only be compared to another view, a constant " + r"with the same layout, or a dictionary or a list that can be converted to " + r"a constant with the same layout, not .*$"): c4 == s1 with self.assertRaisesRegex(TypeError, - r"^Constant with layout .* can only be compared to another view or constant with " - r"the same layout, not .*$"): + r"^Constant with layout .* can only be compared to another view, a constant " + r"with the same layout, or a dictionary or a list that can be converted to " + r"a constant with the same layout, not .*$"): c4 != s1 with self.assertRaisesRegex(TypeError, - r"^Constant with layout .* can only be compared to another view or constant with " - r"the same layout, not .*$"): + r"^Constant with layout .* can only be compared to another view, a constant " + r"with the same layout, or a dictionary or a list that can be converted to " + r"a constant with the same layout, not .*$"): c1 == Const(0, 2) with self.assertRaisesRegex(TypeError, - r"^Constant with layout .* can only be compared to another view or constant with " - r"the same layout, not .*$"): + r"^Constant with layout .* can only be compared to another view, a constant " + r"with the same layout, or a dictionary or a list that can be converted to " + r"a constant with the same layout, not .*$"): c1 != Const(0, 2) + with self.assertRaisesRegex(TypeError, + r"^Constant with layout .* can only be compared to another view, a constant " + r"with the same layout, or a dictionary or a list that can be converted to " + r"a constant with the same layout, not .*$"): + c1 == {"b": 1} + with self.assertRaisesRegex(TypeError, + r"^Constant with layout .* can only be compared to another view, a constant " + r"with the same layout, or a dictionary or a list that can be converted to " + r"a constant with the same layout, not .*$"): + c1 != {"b": 1} + with self.assertRaisesRegex(TypeError, + r"^Constant with layout .* can only be compared to another view, a constant " + r"with the same layout, or a dictionary or a list that can be converted to " + r"a constant with the same layout, not .*$"): + c5 == [0,1,2,3,4] + with self.assertRaisesRegex(TypeError, + r"^Constant with layout .* can only be compared to another view, a constant " + r"with the same layout, or a dictionary or a list that can be converted to " + r"a constant with the same layout, not .*$"): + c5 != [0,1,2,3,4] + + def test_len(self): + c1 = data.Const(data.StructLayout({"a": unsigned(2)}), 2) + with self.assertRaisesRegex(TypeError, + r"^`len\(\)` can only be used on constants of array layout, not StructLayout.*$"): + len(c1) + c2 = data.Const(data.ArrayLayout(2, 3), 0x12) + self.assertEqual(len(c2), 3) def test_operator(self): s1 = data.Const(data.StructLayout({"a": unsigned(2)}), 2) diff --git a/tests/test_lib_enum.py b/tests/test_lib_enum.py index ce0c67d30..1cfe1e231 100644 --- a/tests/test_lib_enum.py +++ b/tests/test_lib_enum.py @@ -107,9 +107,9 @@ def test_const_shape(self): class EnumA(Enum, shape=8): Z = 0 A = 10 - self.assertRepr(EnumA.const(None), "EnumView(EnumA, (const 8'd0))") - self.assertRepr(EnumA.const(10), "EnumView(EnumA, (const 8'd10))") - self.assertRepr(EnumA.const(EnumA.A), "EnumView(EnumA, (const 8'd10))") + self.assertRepr(EnumA.const(None), f"EnumView({EnumA.__qualname__}, (const 8'd0))") + self.assertRepr(EnumA.const(10), f"EnumView({EnumA.__qualname__}, (const 8'd10))") + self.assertRepr(EnumA.const(EnumA.A), f"EnumView({EnumA.__qualname__}, (const 8'd10))") def test_from_bits(self): class EnumA(Enum, shape=2): @@ -138,7 +138,7 @@ class EnumA(IntEnum, shape=signed(4)): B = -3 a = Signal(EnumA) self.assertRepr(a, "(sig a)") - self.assertRepr(a._format, "(format-enum (sig a) 'EnumA' (0 'A') (-3 'B'))") + self.assertRepr(a._format, f"(format-enum (sig a) '{EnumA.__qualname__}' (0 'A') (-3 'B'))") def test_enum_view(self): class EnumA(Enum, shape=signed(4)): @@ -153,7 +153,7 @@ class EnumB(Enum, shape=signed(4)): d = Signal(4) self.assertIsInstance(a, EnumView) self.assertIs(a.shape(), EnumA) - self.assertRepr(a, "EnumView(EnumA, (sig a))") + self.assertRepr(a, f"EnumView({EnumA.__qualname__}, (sig a))") self.assertRepr(a.as_value(), "(sig a)") self.assertRepr(a.eq(c), "(eq (sig a) (sig c))") for op in [ @@ -214,7 +214,7 @@ class FlagB(Flag, shape=unsigned(4)): c = Signal(FlagA) d = Signal(4) self.assertIsInstance(a, FlagView) - self.assertRepr(a, "FlagView(FlagA, (sig a))") + self.assertRepr(a, f"FlagView({FlagA.__qualname__}, (sig a))") for op in [ operator.__add__, operator.__sub__, @@ -260,17 +260,17 @@ class FlagB(Flag, shape=unsigned(4)): self.assertRepr(a == FlagA.B, "(== (sig a) (const 4'd4))") self.assertRepr(FlagA.B == a, "(== (sig a) (const 4'd4))") self.assertRepr(a != FlagA.B, "(!= (sig a) (const 4'd4))") - self.assertRepr(a | c, "FlagView(FlagA, (| (sig a) (sig c)))") - self.assertRepr(a & c, "FlagView(FlagA, (& (sig a) (sig c)))") - self.assertRepr(a ^ c, "FlagView(FlagA, (^ (sig a) (sig c)))") - self.assertRepr(~a, "FlagView(FlagA, (& (~ (sig a)) (const 3'd5)))") - self.assertRepr(a | FlagA.B, "FlagView(FlagA, (| (sig a) (const 4'd4)))") + self.assertRepr(a | c, f"FlagView({FlagA.__qualname__}, (| (sig a) (sig c)))") + self.assertRepr(a & c, f"FlagView({FlagA.__qualname__}, (& (sig a) (sig c)))") + self.assertRepr(a ^ c, f"FlagView({FlagA.__qualname__}, (^ (sig a) (sig c)))") + self.assertRepr(~a, f"FlagView({FlagA.__qualname__}, (& (~ (sig a)) (const 3'd5)))") + self.assertRepr(a | FlagA.B, f"FlagView({FlagA.__qualname__}, (| (sig a) (const 4'd4)))") if sys.version_info >= (3, 11): class FlagC(Flag, shape=unsigned(4), boundary=py_enum.KEEP): A = 1 B = 4 e = Signal(FlagC) - self.assertRepr(~e, "FlagView(FlagC, (~ (sig e)))") + self.assertRepr(~e, f"FlagView({FlagC.__qualname__}, (~ (sig e)))") def test_enum_view_wrong(self): class EnumA(Enum, shape=signed(4)): @@ -327,6 +327,4 @@ class EnumA(Enum, shape=unsigned(2)): B = 1 sig = Signal(EnumA) - self.assertRepr(EnumA.format(sig, ""), """ - (format-enum (sig sig) 'EnumA' (0 'A') (1 'B')) - """) + self.assertRepr(EnumA.format(sig, ""), f"(format-enum (sig sig) '{EnumA.__qualname__}' (0 'A') (1 'B'))") diff --git a/tests/test_lib_io.py b/tests/test_lib_io.py index 9dc208ede..e904e7f41 100644 --- a/tests/test_lib_io.py +++ b/tests/test_lib_io.py @@ -30,6 +30,14 @@ def test_and(self): Direction.Bidir & 3 +class PortLikeTestCase(FHDLTestCase): + def test_warn___add__(self): + with self.assertWarnsRegex(DeprecationWarning, + r"WrongPortLike must override the `__add__` method$"): + class WrongPortLike(PortLike): + pass + + class SingleEndedPortTestCase(FHDLTestCase): def test_construct(self): io = IOPort(4) @@ -161,6 +169,123 @@ def test_invert(self): self.assertRepr(iport, "DifferentialPort((io-port iop), (io-port ion), invert=(False, True, False, True), direction=Direction.Output)") +class SimulationPortTestCase(FHDLTestCase): + def test_construct(self): + port_io = SimulationPort("io", 2) + self.assertEqual(port_io.direction, Direction.Bidir) + self.assertEqual(len(port_io), 2) + self.assertEqual(port_io.invert, (False, False)) + self.assertIsInstance(port_io.i, Signal) + self.assertEqual(port_io.i.shape(), unsigned(2)) + self.assertIsInstance(port_io.o, Signal) + self.assertEqual(port_io.o.shape(), unsigned(2)) + self.assertEqual(port_io.o.init, 0) + self.assertIsInstance(port_io.oe, Signal) + self.assertEqual(port_io.oe.shape(), unsigned(2)) + self.assertEqual(port_io.oe.init, 0) + self.assertRepr(port_io, "SimulationPort(i=(sig port_io__i), o=(sig port_io__o), oe=(sig port_io__oe), invert=False, direction=Direction.Bidir)") + + port_i = SimulationPort("i", 3, invert=True) + self.assertEqual(port_i.direction, Direction.Input) + self.assertEqual(len(port_i), 3) + self.assertEqual(port_i.invert, (True, True, True)) + self.assertIsInstance(port_i.i, Signal) + self.assertEqual(port_i.i.shape(), unsigned(3)) + with self.assertRaisesRegex(AttributeError, + r"^Simulation port with input direction does not have an output signal$"): + port_i.o + with self.assertRaisesRegex(AttributeError, + r"^Simulation port with input direction does not have an output enable signal$"): + port_i.oe + self.assertRepr(port_i, "SimulationPort(i=(sig port_i__i), invert=True, direction=Direction.Input)") + + port_o = SimulationPort("o", 2, invert=(True, False)) + self.assertEqual(port_o.direction, Direction.Output) + self.assertEqual(len(port_o), 2) + self.assertEqual(port_o.invert, (True, False)) + with self.assertRaisesRegex(AttributeError, + r"^Simulation port with output direction does not have an input signal$"): + port_o.i + self.assertIsInstance(port_o.o, Signal) + self.assertEqual(port_o.o.shape(), unsigned(2)) + self.assertEqual(port_o.o.init, 0) + self.assertIsInstance(port_o.oe, Signal) + self.assertEqual(port_o.oe.shape(), unsigned(2)) + self.assertEqual(port_o.oe.init, 0b11) + self.assertRepr(port_o, "SimulationPort(o=(sig port_o__o), oe=(sig port_o__oe), invert=(True, False), direction=Direction.Output)") + + def test_construct_empty(self): + port_i = SimulationPort("i", 0, invert=True) + self.assertEqual(port_i.direction, Direction.Input) + self.assertEqual(len(port_i), 0) + self.assertEqual(port_i.invert, ()) + self.assertIsInstance(port_i.i, Signal) + self.assertEqual(port_i.i.shape(), unsigned(0)) + self.assertRepr(port_i, "SimulationPort(i=(sig port_i__i), invert=False, direction=Direction.Input)") + + def test_name(self): + port = SimulationPort("io", 2, name="nyaa") + self.assertRepr(port, "SimulationPort(i=(sig nyaa__i), o=(sig nyaa__o), oe=(sig nyaa__oe), invert=False, direction=Direction.Bidir)") + + def test_name_wrong(self): + with self.assertRaisesRegex(TypeError, + r"^Name must be a string, not 1$"): + SimulationPort("io", 1, name=1) + + def test_construct_wrong(self): + with self.assertRaisesRegex(TypeError, + r"^Width must be a non-negative integer, not 'a'$"): + SimulationPort("io", "a") + with self.assertRaisesRegex(TypeError, + r"^Width must be a non-negative integer, not -1$"): + SimulationPort("io", -1) + with self.assertRaisesRegex(TypeError, + r"^'invert' must be a bool or iterable of bool, not 3$"): + SimulationPort("io", 1, invert=3) + with self.assertRaisesRegex(TypeError, + r"^'invert' must be a bool or iterable of bool, not \[1, 2\]$"): + SimulationPort("io", 2, invert=[1, 2]) + with self.assertRaisesRegex(ValueError, + r"^Length of 'invert' \(2\) doesn't match port width \(1\)$"): + SimulationPort("io", 1, invert=(False, True)) + + def test_slice(self): + port_io = SimulationPort("io", 2) + self.assertRepr(port_io[0], "SimulationPort(i=(slice (sig port_io__i) 0:1), o=(slice (sig port_io__o) 0:1), oe=(slice (sig port_io__oe) 0:1), invert=False, direction=Direction.Bidir)") + + port_i = SimulationPort("i", 3, invert=True) + self.assertRepr(port_i[1:3], "SimulationPort(i=(slice (sig port_i__i) 1:3), invert=True, direction=Direction.Input)") + + port_o = SimulationPort("o", 2, invert=(True, False)) + self.assertRepr(port_o[1], "SimulationPort(o=(slice (sig port_o__o) 1:2), oe=(slice (sig port_o__oe) 1:2), invert=False, direction=Direction.Output)") + + def test_invert(self): + port_io = SimulationPort("io", 2) + self.assertRepr(~port_io, "SimulationPort(i=(sig port_io__i), o=(sig port_io__o), oe=(sig port_io__oe), invert=True, direction=Direction.Bidir)") + + port_i = SimulationPort("i", 3, invert=True) + self.assertRepr(~port_i, "SimulationPort(i=(sig port_i__i), invert=False, direction=Direction.Input)") + + port_o = SimulationPort("o", 2, invert=(True, False)) + self.assertRepr(~port_o, "SimulationPort(o=(sig port_o__o), oe=(sig port_o__oe), invert=(False, True), direction=Direction.Output)") + + def test_add(self): + port_io = SimulationPort("io", 2) + port_io2 = SimulationPort("io", 2) + port_i = SimulationPort("i", 3, invert=True) + port_o = SimulationPort("o", 2, invert=(True, False)) + + self.assertRepr(port_io + port_io2, "SimulationPort(i=(cat (sig port_io__i) (sig port_io2__i)), o=(cat (sig port_io__o) (sig port_io2__o)), oe=(cat (sig port_io__oe) (sig port_io2__oe)), invert=False, direction=Direction.Bidir)") + self.assertRepr(port_io + port_i, "SimulationPort(i=(cat (sig port_io__i) (sig port_i__i)), invert=(False, False, True, True, True), direction=Direction.Input)") + self.assertRepr(port_io + port_o, "SimulationPort(o=(cat (sig port_io__o) (sig port_o__o)), oe=(cat (sig port_io__oe) (sig port_o__oe)), invert=(False, False, True, False), direction=Direction.Output)") + + def test_add_wrong(self): + io = IOPort(1) + with self.assertRaisesRegex(TypeError, + r"^unsupported operand type\(s\) for \+: 'SimulationPort' and 'SingleEndedPort'$"): + SimulationPort("io", 2) + SingleEndedPort(io) + + class BufferTestCase(FHDLTestCase): def test_signature(self): sig_i = Buffer.Signature("i", 4) @@ -378,6 +503,121 @@ def test_elaborate_diff(self): ) """) + def test_elaborate_sim(self): + port = SimulationPort("io", 4) + buf = Buffer("io", port) + nl = build_netlist(Fragment.get(buf, None), [buf.i, buf.o, buf.oe, port.i, port.o, port.oe]) + self.assertRepr(nl, """ + ( + (module 0 None ('top') + (input 'o' 0.2:6) + (input 'oe' 0.6) + (input 'port__i' 0.7:11) + (output 'i' 5.0:4) + (output 'port__o' 0.2:6) + (output 'port__oe' (cat 0.6 0.6 0.6 0.6)) + ) + (cell 0 0 (top + (input 'o' 2:6) + (input 'oe' 6:7) + (input 'port__i' 7:11) + (output 'i' 5.0:4) + (output 'port__o' 0.2:6) + (output 'port__oe' (cat 0.6 0.6 0.6 0.6)) + )) + (cell 1 0 (m 0.6 0.2 0.7)) + (cell 2 0 (m 0.6 0.3 0.8)) + (cell 3 0 (m 0.6 0.4 0.9)) + (cell 4 0 (m 0.6 0.5 0.10)) + (cell 5 0 (assignment_list 4'd0 (1 0:1 1.0) (1 1:2 2.0) (1 2:3 3.0) (1 3:4 4.0))) + ) + """) + + port = SimulationPort("io", 4, invert=[False, True, False, True]) + buf = Buffer("io", port) + nl = build_netlist(Fragment.get(buf, None), [buf.i, buf.o, buf.oe, port.i, port.o, port.oe]) + self.assertRepr(nl, """ + ( + (module 0 None ('top') + (input 'o' 0.2:6) + (input 'oe' 0.6) + (input 'port__i' 0.7:11) + (output 'i' 2.0:4) + (output 'port__o' 1.0:4) + (output 'port__oe' (cat 0.6 0.6 0.6 0.6)) + ) + (cell 0 0 (top + (input 'o' 2:6) + (input 'oe' 6:7) + (input 'port__i' 7:11) + (output 'i' 2.0:4) + (output 'port__o' 1.0:4) + (output 'port__oe' (cat 0.6 0.6 0.6 0.6)) + )) + (cell 1 0 (^ 0.2:6 4'd10)) + (cell 2 0 (^ 7.0:4 4'd10)) + (cell 3 0 (m 0.6 1.0 0.7)) + (cell 4 0 (m 0.6 1.1 0.8)) + (cell 5 0 (m 0.6 1.2 0.9)) + (cell 6 0 (m 0.6 1.3 0.10)) + (cell 7 0 (assignment_list 4'd0 (1 0:1 3.0) (1 1:2 4.0) (1 2:3 5.0) (1 3:4 6.0))) + ) + """) + + buf = Buffer("i", port) + nl = build_netlist(Fragment.get(buf, None), [buf.i, port.i]) + self.assertRepr(nl, """ + ( + (module 0 None ('top') + (input 'port__i' 0.2:6) + (output 'i' 1.0:4) + ) + (cell 0 0 (top + (input 'port__i' 2:6) + (output 'i' 1.0:4) + )) + (cell 1 0 (^ 0.2:6 4'd10)) + ) + """) + + buf = Buffer("o", port) + nl = build_netlist(Fragment.get(buf, None), [buf.o, buf.oe, port.o, port.oe]) + self.assertRepr(nl, """ + ( + (module 0 None ('top') + (input 'o' 0.2:6) + (input 'oe' 0.6) + (output 'port__o' 1.0:4) + (output 'port__oe' (cat 0.6 0.6 0.6 0.6)) + ) + (cell 0 0 (top + (input 'o' 2:6) + (input 'oe' 6:7) + (output 'port__o' 1.0:4) + (output 'port__oe' (cat 0.6 0.6 0.6 0.6)) + )) + (cell 1 0 (^ 0.2:6 4'd10)) + ) + """) + + # check that a port without `port.o`/`port.oe` works + port = SimulationPort("i", 4, invert=[False, True, False, True]) + buf = Buffer("i", port) + nl = build_netlist(Fragment.get(buf, None), [buf.i, port.i]) + self.assertRepr(nl, """ + ( + (module 0 None ('top') + (input 'port__i' 0.2:6) + (output 'i' 1.0:4) + ) + (cell 0 0 (top + (input 'port__i' 2:6) + (output 'i' 1.0:4) + )) + (cell 1 0 (^ 0.2:6 4'd10)) + ) + """) + class FFBufferTestCase(FHDLTestCase): def test_signature(self): @@ -616,6 +856,150 @@ def test_elaborate(self): ) """) + def test_elaborate_sim(self): + port = SimulationPort("io", 4) + buf = FFBuffer("io", port) + nl = build_netlist(Fragment.get(buf, None), [buf.i, buf.o, buf.oe, port.i, port.o, port.oe]) + self.assertRepr(nl, """ + ( + (module 0 None ('top') + (input 'o' 0.2:6) + (input 'oe' 0.6) + (input 'port__i' 0.7:11) + (input 'clk' 0.11) + (input 'rst' 0.12) + (output 'i' 5.0:4) + (output 'port__o' 6.0:4) + (output 'port__oe' (cat 7.0 7.0 7.0 7.0)) + ) + (module 1 0 ('top' 'io_buffer') + (input 'port__i' 0.7:11) + (input 'port__o' 6.0:4) + (input 'oe' 7.0) + (output 'i' 8.0:4) + ) + (cell 0 0 (top + (input 'o' 2:6) + (input 'oe' 6:7) + (input 'port__i' 7:11) + (input 'clk' 11:12) + (input 'rst' 12:13) + (output 'i' 5.0:4) + (output 'port__o' 6.0:4) + (output 'port__oe' (cat 7.0 7.0 7.0 7.0)) + )) + (cell 1 1 (m 7.0 6.0 0.7)) + (cell 2 1 (m 7.0 6.1 0.8)) + (cell 3 1 (m 7.0 6.2 0.9)) + (cell 4 1 (m 7.0 6.3 0.10)) + (cell 5 0 (flipflop 8.0:4 0 pos 0.11 0)) + (cell 6 0 (flipflop 0.2:6 0 pos 0.11 0)) + (cell 7 0 (flipflop 0.6 0 pos 0.11 0)) + (cell 8 1 (assignment_list 4'd0 (1 0:1 1.0) (1 1:2 2.0) (1 2:3 3.0) (1 3:4 4.0))) + ) + """) + + port = SimulationPort("io", 4, invert=[False, True, False, True]) + buf = FFBuffer("io", port) + nl = build_netlist(Fragment.get(buf, None), [buf.i, buf.o, buf.oe, port.i, port.o, port.oe]) + self.assertRepr(nl, """ + ( + (module 0 None ('top') + (input 'o' 0.2:6) + (input 'oe' 0.6) + (input 'port__i' 0.7:11) + (input 'clk' 0.11) + (input 'rst' 0.12) + (output 'i' 7.0:4) + (output 'port__o' 1.0:4) + (output 'port__oe' (cat 9.0 9.0 9.0 9.0)) + ) + (module 1 0 ('top' 'io_buffer') + (input 'port__i' 0.7:11) + (output 'o_inv' 1.0:4) + (output 'i' 2.0:4) + (input 'o' 8.0:4) + (input 'oe' 9.0) + ) + (cell 0 0 (top + (input 'o' 2:6) + (input 'oe' 6:7) + (input 'port__i' 7:11) + (input 'clk' 11:12) + (input 'rst' 12:13) + (output 'i' 7.0:4) + (output 'port__o' 1.0:4) + (output 'port__oe' (cat 9.0 9.0 9.0 9.0)) + )) + (cell 1 1 (^ 8.0:4 4'd10)) + (cell 2 1 (^ 10.0:4 4'd10)) + (cell 3 1 (m 9.0 1.0 0.7)) + (cell 4 1 (m 9.0 1.1 0.8)) + (cell 5 1 (m 9.0 1.2 0.9)) + (cell 6 1 (m 9.0 1.3 0.10)) + (cell 7 0 (flipflop 2.0:4 0 pos 0.11 0)) + (cell 8 0 (flipflop 0.2:6 0 pos 0.11 0)) + (cell 9 0 (flipflop 0.6 0 pos 0.11 0)) + (cell 10 1 (assignment_list 4'd0 (1 0:1 3.0) (1 1:2 4.0) (1 2:3 5.0) (1 3:4 6.0))) + ) + """) + + buf = FFBuffer("i", port) + nl = build_netlist(Fragment.get(buf, None), [buf.i, port.i]) + self.assertRepr(nl, """ + ( + (module 0 None ('top') + (input 'port__i' 0.2:6) + (input 'clk' 0.6) + (input 'rst' 0.7) + (output 'i' 2.0:4) + ) + (module 1 0 ('top' 'io_buffer') + (input 'i_inv' 0.2:6) + (output 'i' 1.0:4) + ) + (cell 0 0 (top + (input 'port__i' 2:6) + (input 'clk' 6:7) + (input 'rst' 7:8) + (output 'i' 2.0:4) + )) + (cell 1 1 (^ 0.2:6 4'd10)) + (cell 2 0 (flipflop 1.0:4 0 pos 0.6 0)) + ) + """) + + buf = FFBuffer("o", port) + nl = build_netlist(Fragment.get(buf, None), [buf.o, buf.oe, port.o, port.oe]) + self.assertRepr(nl, """ + ( + (module 0 None ('top') + (input 'o' 0.2:6) + (input 'oe' 0.6) + (input 'clk' 0.7) + (input 'rst' 0.8) + (output 'port__o' 1.0:4) + (output 'port__oe' (cat 3.0 3.0 3.0 3.0)) + ) + (module 1 0 ('top' 'io_buffer') + (output 'o_inv' 1.0:4) + (input 'o' 2.0:4) + (input 'oe' 3.0) + ) + (cell 0 0 (top + (input 'o' 2:6) + (input 'oe' 6:7) + (input 'clk' 7:8) + (input 'rst' 8:9) + (output 'port__o' 1.0:4) + (output 'port__oe' (cat 3.0 3.0 3.0 3.0)) + )) + (cell 1 1 (^ 2.0:4 4'd10)) + (cell 2 0 (flipflop 0.2:6 0 pos 0.7 0)) + (cell 3 0 (flipflop 0.6 0 pos 0.7 0)) + ) + """) + class DDRBufferTestCase(FHDLTestCase): def test_signature(self): diff --git a/tests/test_lib_memory.py b/tests/test_lib_memory.py index 3700613f3..ba828a8b3 100644 --- a/tests/test_lib_memory.py +++ b/tests/test_lib_memory.py @@ -329,7 +329,7 @@ def test_constructor_wrong(self): memory.Memory(shape="a", depth=3, init=[]) with self.assertRaisesRegex(TypeError, (r"^Memory initialization value at address 1: " - r"'str' object cannot be interpreted as an integer$")): + r"Initial value must be a constant-castable expression, not '0'$")): memory.Memory(shape=8, depth=4, init=[1, "0"]) with self.assertRaisesRegex(ValueError, r"^Either 'data' or 'shape' needs to be given$"): @@ -373,7 +373,7 @@ def test_init_set_shapecastable(self): def test_init_set_wrong(self): m = memory.Memory(shape=8, depth=4, init=[]) with self.assertRaisesRegex(TypeError, - r"^'str' object cannot be interpreted as an integer$"): + r"^Initial value must be a constant-castable expression, not 'a'$"): m.init[0] = "a" m = memory.Memory(shape=MyStruct, depth=4, init=[]) # underlying TypeError message differs between PyPy and CPython diff --git a/tests/test_lib_stream.py b/tests/test_lib_stream.py index 17ce59684..ce45447e1 100644 --- a/tests/test_lib_stream.py +++ b/tests/test_lib_stream.py @@ -108,6 +108,11 @@ def test_eq(self): self.assertNotEqual(sig_av_ar, sig_nav_nar) self.assertNotEqual(sig_av_ar, sig_av_ar2) + def test_payload_init(self): + sig = stream.Signature(2, payload_init=0b10) + intf = sig.create() + self.assertEqual(intf.payload.init, 0b10) + def test_interface_create_bad(self): with self.assertRaisesRegex(TypeError, r"^Signature of stream\.Interface must be a stream\.Signature, not " diff --git a/tests/test_lib_wiring.py b/tests/test_lib_wiring.py index d4b7d8cc5..eb2639047 100644 --- a/tests/test_lib_wiring.py +++ b/tests/test_lib_wiring.py @@ -635,7 +635,7 @@ def test_repr_inherit(self): class CustomInterface(PureInterface): pass intf = CustomInterface(Signature({}), path=()) - self.assertRegex(repr(intf), r"^<CustomInterface: .+?>$") + self.assertRegex(repr(intf), r"^<.+\.CustomInterface: .+?>$") class FlippedInterfaceTestCase(unittest.TestCase): @@ -1045,6 +1045,15 @@ def test_connect_multi_some_in_pairs(self): a=Signal(), b=Signal())) + def test_bug_1435_missing_module(self): + sig = Signature({"a": Out(1)}) + p = sig.create() + q = sig.flip().create() + + with self.assertRaisesRegex(TypeError, + r"^The initial argument must be a module, not <PureInterface: .+>$"): + connect(p, q) + class ComponentTestCase(unittest.TestCase): def test_basic(self): diff --git a/tests/test_sim.py b/tests/test_sim.py index 612068d65..86e8e284b 100644 --- a/tests/test_sim.py +++ b/tests/test_sim.py @@ -727,8 +727,8 @@ def test_add_process_wrong(self): def test_add_process_wrong_generator(self): with self.assertSimulation(Module()) as sim: with self.assertRaisesRegex(TypeError, - r"^Cannot add a process <.+?> because it is not an async function or " - r"generator function$"): + r"^Cannot add a process <.+?> because it is a generator object instead of " + r"a function \(pass the function itself instead of calling it\)$"): def process(): yield Delay() sim.add_process(process()) @@ -743,12 +743,39 @@ def test_add_testbench_wrong(self): def test_add_testbench_wrong_generator(self): with self.assertSimulation(Module()) as sim: with self.assertRaisesRegex(TypeError, - r"^Cannot add a testbench <.+?> because it is not an async function or " - r"generator function$"): + r"^Cannot add a testbench <.+?> because it is a generator object instead of " + r"a function \(pass the function itself instead of calling it\)$"): def testbench(): yield Delay() sim.add_testbench(testbench()) + def test_add_testbench_wrong_coroutine(self): + with self.assertSimulation(Module()) as sim: + with self.assertRaisesRegex(TypeError, + r"^Cannot add a testbench <.+?> because it is a coroutine object instead of " + r"a function \(pass the function itself instead of calling it\)$"): + async def testbench(): + pass + sim.add_testbench(testbench()) + + def test_add_testbench_wrong_async_generator(self): + with self.assertSimulation(Module()) as sim: + with self.assertRaisesRegex(TypeError, + r"^Cannot add a testbench <.+?> because it is a generator object instead of " + r"a function \(pass the function itself instead of calling it\)$"): + async def testbench(): + yield Delay() + sim.add_testbench(testbench()) + + def test_add_testbench_wrong_async_generator_func(self): + with self.assertSimulation(Module()) as sim: + with self.assertRaisesRegex(TypeError, + r"^Cannot add a testbench <.+?> because it is an async generator function " + r"\(there is likely a stray `yield` in the function\)$"): + async def testbench(): + yield Delay() + sim.add_testbench(testbench) + def test_add_clock_wrong_twice(self): m = Module() s = Signal() @@ -1247,6 +1274,33 @@ async def testbench(ctx): [unknown] """)) + def test_print_enum_followed_by(self): + class MyEnum(enum.Enum, shape=unsigned(2)): + A = 0 + B = 1 + CDE = 2 + + sig = Signal(MyEnum) + ctr = Signal(2) + m = Module() + m.d.comb += sig.eq(ctr) + m.d.sync += [ + Print(Format("{} {}", sig, ctr)), + ctr.eq(ctr + 1), + ] + output = StringIO() + with redirect_stdout(output): + with self.assertSimulation(m) as sim: + sim.add_clock(1e-6, domain="sync") + async def testbench(ctx): + await ctx.tick().repeat(4) + sim.add_testbench(testbench) + self.assertEqual(output.getvalue(), dedent("""\ + A 0 + B 1 + CDE 2 + [unknown] 3 + """)) def test_assert(self): m = Module() @@ -1375,15 +1429,42 @@ class MyEnum(enum.Enum, shape=2): mem1 = MemoryData(shape=8, depth=4, init=[1, 2, 3]) mem2 = MemoryData(shape=MyEnum, depth=4, init=[MyEnum.A, MyEnum.B, MyEnum.C]) mem3 = MemoryData(shape=data.StructLayout({"a": signed(3), "b": 2}), depth=4, init=[{"a": 2, "b": 1}]) + mem4 = MemoryData(shape=signed(8), depth=4, init=[1, -2, 3]) async def testbench(ctx): await ctx.delay(1e-6) ctx.set(mem1[0], 4) ctx.set(mem2[3], MyEnum.C) ctx.set(mem3[2], {"a": -1, "b": 2}) + ctx.set(mem4[1][4:], 0) + ctx.set(mem4[3][7], 1) await ctx.delay(1e-6) + self.assertEqual(ctx.get(mem4[1]), 0xe) + self.assertEqual(ctx.get(mem4[3]), -128) - with self.assertSimulation(Module(), traces=[mem1, mem2, mem3]) as sim: + with self.assertSimulation(Module(), traces=[mem1, mem2, mem3, mem4]) as sim: + sim.add_testbench(testbench) + + def test_multiple_modules(self): + m = Module() + m.submodules.m1 = m1 = Module() + m.submodules.m2 = m2 = Module() + a = Signal(8) + b = Signal(8) + m1.d.comb += b[0:2].eq(a[0:2]) + m1.d.comb += b[4:6].eq(a[4:6]) + m2.d.comb += b[2:4].eq(a[2:4]) + m2.d.comb += b[6:8].eq(a[6:8]) + with self.assertSimulation(m) as sim: + async def testbench(ctx): + ctx.set(a, 0) + self.assertEqual(ctx.get(b), 0) + ctx.set(a, 0x12) + self.assertEqual(ctx.get(b), 0x12) + ctx.set(a, 0x34) + self.assertEqual(ctx.get(b), 0x34) + ctx.set(a, 0xdb) + self.assertEqual(ctx.get(b), 0xdb) sim.add_testbench(testbench) @@ -1403,6 +1484,10 @@ def test_signal(self): a = Signal() self.assertDef(a, [a]) + def test_expr(self): + a = Signal() + self.assertDef(a + 3, [a]) + def test_list(self): a = Signal() self.assertDef([a], [a]) @@ -2026,15 +2111,6 @@ async def testbench(ctx): self.assertTrue(reached_tb) self.assertTrue(reached_proc) - def test_bug_1363(self): - sim = Simulator(Module()) - with self.assertRaisesRegex(TypeError, - r"^Cannot add a testbench <.+?> because it is not an async function or " - r"generator function$"): - async def testbench(): - yield Delay() - sim.add_testbench(testbench()) - def test_issue_1368(self): sim = Simulator(Module()) async def testbench(ctx): diff --git a/tests/utils.py b/tests/utils.py index a069f18fb..79f8f081b 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -16,15 +16,57 @@ class FHDLTestCase(unittest.TestCase): + maxDiff = None + def assertRepr(self, obj, repr_str): if isinstance(obj, list): obj = Statement.cast(obj) - def prepare_repr(repr_str): + def squish_repr(repr_str): repr_str = re.sub(r"\s+", " ", repr_str) repr_str = re.sub(r"\( (?=\()", "(", repr_str) repr_str = re.sub(r"\) (?=\))", ")", repr_str) return repr_str.strip() - self.assertEqual(prepare_repr(repr(obj)), prepare_repr(repr_str)) + def format_repr(input_repr, *, indent=" "): + output_repr = [] + prefix = "\n" + name = None + index = 0 + stack = [] + current = "" + for char in input_repr: + if char == "(": + stack.append((prefix, name, index)) + name, index = None, 0 + output_repr.append(char) + if len(stack) == 1: + prefix += indent + output_repr.append(prefix) + elif char == ")": + indented = (len(stack) == 1 or name in ("module", "top")) + prefix, name, index = stack.pop() + if indented: + output_repr.append(prefix) + output_repr.append(char) + elif char == " ": + if name is None: + name = current + if name in ("module", "top"): + prefix += indent + else: + index += 1 + current = "" + if len(stack) == 1 or name == "module" and index >= 3 or name == "top": + output_repr.append(prefix) + else: + output_repr.append(char) + elif name is None: + current += char + output_repr.append(char) + else: + output_repr.append(char) + return "".join(output_repr) + # print("\n" + format_repr(squish_repr(repr(obj)))) + self.assertEqual(format_repr(squish_repr(repr(obj))), format_repr(squish_repr(repr_str))) def assertFormal(self, spec, ports=None, mode="bmc", depth=1): stack = traceback.extract_stack() @@ -62,7 +104,7 @@ def assertFormal(self, spec, ports=None, mode="bmc", depth=1): smtbmc [script] - read_ilang top.il + read_rtlil top.il prep {script}