• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nbiotcloud / ucdp / 19922897232

04 Dec 2025 08:44AM UTC coverage: 90.264% (-0.04%) from 90.305%
19922897232

push

github

web-flow
Merge pull request #144 from nbiotcloud/143-add-support-for-defines-on-modules

143 add support for defines on modules

72 of 81 new or added lines in 4 files covered. (88.89%)

4821 of 5341 relevant lines covered (90.26%)

13.49 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.23
/src/ucdp/modbase.py
1
#
2
# MIT License
3
#
4
# Copyright (c) 2024-2025 nbiotcloud
5
#
6
# Permission is hereby granted, free of charge, to any person obtaining a copy
7
# of this software and associated documentation files (the "Software"), to deal
8
# in the Software without restriction, including without limitation the rights
9
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
# copies of the Software, and to permit persons to whom the Software is
11
# furnished to do so, subject to the following conditions:
12
#
13
# The above copyright notice and this permission notice shall be included in all
14
# copies or substantial portions of the Software.
15
#
16
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
# SOFTWARE.
23
#
24
"""
25
Base Hardware Module.
26

27
[BaseMod][ucdp.modbase.BaseMod] defines the base interface which is **common to all hardware modules**.
28
"""
29

30
import warnings
15✔
31
from abc import abstractmethod
15✔
32
from functools import cached_property
15✔
33
from inspect import getmro
15✔
34
from typing import Annotated, Any, ClassVar, Literal, Optional, TypeAlias, Union, no_type_check
15✔
35

36
from aligntext import align
15✔
37
from caseconverter import snakecase
15✔
38
from pydantic import PlainValidator
15✔
39
from uniquer import uniquetuple
15✔
40

41
from .assigns import Assigns, Drivers, Note, Source
15✔
42
from .baseclassinfo import get_baseclassinfos
15✔
43
from .clkrel import ClkRel
15✔
44
from .clkrelbase import BaseClkRel
15✔
45
from .const import Const
15✔
46
from .consts import UPWARDS
15✔
47
from .define import Define, Defines
15✔
48
from .doc import Doc
15✔
49
from .docutil import doc_from_type
15✔
50
from .exceptions import LockError
15✔
51
from .expr import BoolOp, Expr
15✔
52
from .exprparser import ExprParser, Parseable, cast_booltype
15✔
53
from .flipflop import FlipFlop
15✔
54
from .ident import Ident, Idents
15✔
55
from .ifdef import Ifdefs, cast_ifdefs, resolve_ifdefs
15✔
56
from .iterutil import namefilter
15✔
57
from .logging import LOGGER
15✔
58
from .modref import ModRef, get_modclsname
15✔
59
from .modutil import get_modbaseinfos
15✔
60
from .mux import Mux
15✔
61
from .namespace import Namespace
15✔
62
from .nameutil import join_names, split_prefix
15✔
63
from .object import Field, NamedObject, Object, PrivateField, computed_field
15✔
64
from .orientation import FWD, IN, Direction, Orientation
15✔
65
from .param import Param
15✔
66
from .routepath import Routeables, RoutePath, parse_routepaths
15✔
67
from .signal import BaseSignal, Port, Signal
15✔
68
from .typebase import BaseType
15✔
69
from .typedescriptivestruct import DescriptiveStructType
15✔
70
from .typestruct import StructItem
15✔
71

72
ModTags: TypeAlias = set[str]
15✔
73
RoutingError: TypeAlias = Literal["error", "warn", "ignore"]
15✔
74

75

76
def _to_defines(value):
15✔
77
    if value is None:
15✔
78
        return Namespace()
15✔
79
    if isinstance(value, Namespace):
15✔
NEW
80
        value.lock()
×
NEW
81
        return value
×
82
    if isinstance(value, dict):
15✔
83
        defines = Namespace()
15✔
84
        for key, val in value.items():
15✔
85
            defines.add(Define(key, value=val))
15✔
86
        return defines
15✔
NEW
87
    raise ValueError(value)
×
88

89

90
class BaseMod(NamedObject):
15✔
91
    """
92
    Hardware Module.
93

94
    Args:
95
        parent: Parent Module. `None` by default for top module.
96
        name: Instance name. Required if parent is provided.
97

98
    Keyword Args:
99
        title (str): Title
100
        descr (str): Description
101
        comment (str): Comment
102
        paramdict (dict): Parameter values for this instance.
103
    """
104

105
    filelists: ClassVar[Any] = ()
15✔
106
    tags: ClassVar[ModTags] = ModTags()
15✔
107

108
    parent: Optional["BaseMod"] = None
15✔
109
    paramdict: dict = Field(default_factory=dict, repr=False)
15✔
110

111
    title: str | None = None
15✔
112
    descr: str | None = None
15✔
113
    comment: str | None = None
15✔
114

115
    has_hiername: bool = True
15✔
116

117
    virtual: bool = False
15✔
118

119
    # private
120

121
    drivers: Drivers = Field(default_factory=Drivers, init=False, repr=False)
15✔
122
    defines: Annotated[Defines, PlainValidator(_to_defines)] = Field(default_factory=Defines)
15✔
123
    namespace: Idents = Field(default_factory=Idents, init=False, repr=False)
15✔
124
    params: Idents = Field(default_factory=Idents, init=False, repr=False)
15✔
125
    ports: Idents = Field(default_factory=Idents, init=False, repr=False)
15✔
126
    portssignals: Idents = Field(default_factory=Idents, init=False, repr=False)
15✔
127
    insts: Namespace = Field(default_factory=Namespace, init=False, repr=False)
15✔
128

129
    _has_build_dep: bool = PrivateField(default=False)
15✔
130
    _has_build_final: bool = PrivateField(default=False)
15✔
131

132
    __is_locked: bool = PrivateField(default=False)
15✔
133
    __instcons: dict[str, tuple[Assigns, ExprParser]] = PrivateField(default_factory=dict)
15✔
134
    __flipflops: dict[int, FlipFlop] = PrivateField(default_factory=dict)
15✔
135
    __muxes: Namespace = PrivateField(default_factory=Namespace)
15✔
136
    __parents = PrivateField(default_factory=list)
15✔
137

138
    def __init__(self, parent: Optional["BaseMod"] = None, name: str | None = None, defines=None, **kwargs):
15✔
139
        cls = self.__class__
15✔
140
        if not cls.__name__.endswith("Mod"):
15✔
141
            raise NameError(f"Name of {cls} MUST end with 'Mod'")
15✔
142
        if not name:
15✔
143
            if parent:
15✔
144
                raise ValueError("'name' is required for sub modules.")
15✔
145
            name = snakecase(cls.__name__.removesuffix("Mod"))
15✔
146
        super().__init__(parent=parent, name=name, defines=defines, **kwargs)  # type: ignore[call-arg]
15✔
147

148
    @property
15✔
149
    def doc(self) -> Doc:
15✔
150
        """Documentation."""
151
        return Doc(title=self.title, descr=self.descr, comment=self.comment)
15✔
152

153
    @property
15✔
154
    def basename(self) -> str:
15✔
155
        """Base Name Derived From Instance."""
156
        return split_prefix(self.name)[1]
15✔
157

158
    @property
15✔
159
    @abstractmethod
15✔
160
    def modname(self) -> str:
15✔
161
        """Module Name."""
162

163
    @property
15✔
164
    @abstractmethod
15✔
165
    def topmodname(self) -> str:
15✔
166
        """Top Module Name."""
167

168
    @property
15✔
169
    def libname(self) -> str:
15✔
170
        """Library Name."""
171
        return self.libpath.name
15✔
172

173
    @property
15✔
174
    @abstractmethod
15✔
175
    def libpath(self) -> str:
15✔
176
        """Library Path."""
177

178
    @cached_property
15✔
179
    def qualname(self) -> str:
15✔
180
        """Qualified Name (Library Name + Module Name)."""
181
        return f"{self.libname}.{self.modname}"
15✔
182

183
    @cached_property
15✔
184
    def basequalnames(self) -> tuple[str, ...]:
15✔
185
        """Qualified Name (Library Name + Module Name) of Base Modules."""
186
        return uniquetuple(f"{bci.libname}.{bci.modname}" for bci in get_modbaseinfos(self))
15✔
187

188
    @classmethod
15✔
189
    def get_modref(cls, minimal: bool = False) -> ModRef:
15✔
190
        """Python Class Reference."""
191
        bci = next(get_baseclassinfos(cls))
15✔
192
        modclsname = bci.clsname if not minimal or bci.clsname != get_modclsname(bci.modname) else None
15✔
193

194
        return ModRef(
15✔
195
            libname=bci.libname,
196
            modname=bci.modname,
197
            modclsname=modclsname,
198
        )
199

200
    @classmethod
15✔
201
    def get_basemodrefs(cls) -> tuple[ModRef, ...]:
15✔
202
        """Python Class Reference."""
203
        return tuple(
15✔
204
            ModRef(
205
                libname=bci.libname,
206
                modname=bci.modname,
207
                modclsname=bci.clsname,
208
            )
209
            for bci in get_modbaseinfos(cls)
210
        )
211

212
    @property
15✔
213
    def hiername(self) -> str:
15✔
214
        """Hierarchical Name."""
215
        mod: BaseMod | None = self
15✔
216
        names: list[str] = []
15✔
217
        while mod is not None:
15✔
218
            if mod.has_hiername:
15✔
219
                names.insert(0, split_prefix(mod.name)[1])
15✔
220
            mod = mod.parent
15✔
221
        return join_names(*names)
15✔
222

223
    @property
15✔
224
    @abstractmethod
15✔
225
    def is_tb(self) -> bool:
15✔
226
        """Determine if module belongs to Testbench or Design."""
227

228
    @property
15✔
229
    def path(self) -> tuple["BaseMod", ...]:
15✔
230
        """Path."""
231
        path = [self]
15✔
232
        parent = self.parent
15✔
233
        while parent:
15✔
234
            path.insert(0, parent)
15✔
235
            parent = parent.parent
15✔
236
        return tuple(path)
15✔
237

238
    @property
15✔
239
    def inst(self) -> str:
15✔
240
        """Path String."""
241
        parts: list[str] = []
15✔
242
        mod = self
15✔
243
        while mod.parent:
15✔
244
            parts.insert(0, mod.name)
15✔
245
            mod = mod.parent
15✔
246
        parts.insert(0, mod.modname)
15✔
247
        return "/".join(parts)
15✔
248

249
    @computed_field
15✔
250
    @cached_property
15✔
251
    def assigns(self) -> Assigns:
15✔
252
        """Assignments."""
253
        return Assigns(targets=self.portssignals, sources=self.namespace, drivers=self.drivers)
×
254

255
    @computed_field
15✔
256
    @cached_property
15✔
257
    def parser(self) -> ExprParser:
15✔
258
        """Expression Parser."""
259
        return ExprParser(namespace=self.namespace, context=str(self))
15✔
260

261
    @computed_field
15✔
262
    @cached_property
15✔
263
    def _router(self) -> "Router":
15✔
264
        """Router."""
265
        return Router(mod=self)
15✔
266

267
    @property
15✔
268
    def parents(self) -> tuple["BaseMod", ...]:
15✔
269
        """Parents."""
270
        return tuple(self.__parents)
15✔
271

272
    @classmethod
15✔
273
    def build_top(cls, **kwargs) -> "BaseMod":
15✔
274
        """
275
        Build Top Instance.
276

277
        Return module as top module.
278
        """
279
        return cls(**kwargs)
15✔
280

281
    def add_param(
15✔
282
        self,
283
        arg: BaseType | Param,
284
        name: str | None = None,
285
        title: str | None = None,
286
        descr: str | None = None,
287
        comment: str | None = None,
288
        ifdef: str | None = None,
289
        ifdefs: Ifdefs | str | None = None,
290
        exist_ok: bool = False,
291
    ) -> Param | None:
292
        """
293
        Add Module Parameter (:any:`Param`).
294

295
        Args:
296
            arg: Type or Parameter
297
            name: Name. Mandatory if arg is a Type.
298

299
        Keyword Args:
300
            title: Full Spoken Name.
301
            descr: Documentation Description.
302
            comment: Source Code Comment.
303
            ifdef: IFDEF pragma. Obsolete.
304
            ifdefs: IFDEFs pragmas.
305
            exist_ok: Do not complain about already existing item
306
        """
307
        if ifdef:
15✔
308
            warnings.warn(
×
309
                "add_param(..., ifdef=...) is obsolete, please use ifdefs=", category=DeprecationWarning, stacklevel=1
310
            )
311
        ifdefs = cast_ifdefs(ifdefs or ifdef)
15✔
312
        rifdefs = resolve_ifdefs(self.defines, ifdefs)
15✔
313
        if rifdefs is None:
15✔
NEW
314
            return None
×
315
        if isinstance(arg, Param):
15✔
316
            value = self.paramdict.pop(arg.name, None)
15✔
317
            param: Param = arg.new(value=value)
15✔
318
            assert name is None
15✔
319
        else:
320
            type_: BaseType = arg
15✔
321
            doc = doc_from_type(type_, title=title, descr=descr, comment=comment)
15✔
322
            value = self.paramdict.pop(name, None)
15✔
323
            param = Param(type_=type_, name=name, doc=doc, ifdefs=rifdefs, value=value)
15✔
324
        if self.__is_locked:
15✔
325
            raise LockError(f"{self}: Cannot add parameter {name!r}.")
15✔
326
        self.namespace.add(param, exist_ok=exist_ok)
15✔
327
        self.params.add(param, exist_ok=exist_ok)
15✔
328
        return param
15✔
329

330
    def add_const(
15✔
331
        self,
332
        arg: BaseType | Const,
333
        name: str | None = None,
334
        title: str | None = None,
335
        descr: str | None = None,
336
        comment: str | None = None,
337
        ifdef: str | None = None,
338
        ifdefs: Ifdefs | str | None = None,
339
        exist_ok: bool = False,
340
    ) -> Const | None:
341
        """
342
        Add Module Internal Constant (:any:`Const`).
343

344
        Args:
345
            arg: Type or Parameter
346
            name: Name. Mandatory if arg is a Type.
347

348
        Keyword Args:
349
            title: Full Spoken Name.
350
            descr: Documentation Description.
351
            comment: Source Code Comment.
352
            ifdef: IFDEF pragma. Obsolete.
353
            ifdefs: IFDEFs pragmas.
354
            exist_ok: Do not complain about already existing item
355
        """
356
        if ifdef:
15✔
357
            warnings.warn(
×
358
                "add_const(..., ifdef=...) is obsolete, please use ifdefs=", category=DeprecationWarning, stacklevel=1
359
            )
360
        ifdefs = cast_ifdefs(ifdefs or ifdef)
15✔
361
        rifdefs = resolve_ifdefs(self.defines, ifdefs)
15✔
362
        if rifdefs is None:
15✔
NEW
363
            return None
×
364
        if isinstance(arg, Const):
15✔
365
            const: Const = arg
15✔
366
            assert name is None
15✔
367
        else:
368
            type_: BaseType = arg
15✔
369
            doc = doc_from_type(type_, title=title, descr=descr, comment=comment)
15✔
370
            const = Const(type_=type_, name=name, doc=doc, ifdefs=rifdefs)
15✔
371
        if self.__is_locked:
15✔
372
            raise LockError(f"{self}: Cannot add constant {name!r}.")
15✔
373
        self.namespace.add(const, exist_ok=exist_ok)
15✔
374
        return const
15✔
375

376
    def add_type_consts(self, type_: BaseType, exist_ok: bool = False, only=None, name=None, item_suffix="e"):
15✔
377
        """
378
        Add description of `type_` as local parameters.
379

380
        Args:
381
            type_: Type to be described.
382

383
        Keyword Args:
384
            exist_ok (bool): Do not complain, if parameter already exists.
385
            name (str): Name of the local parameter. Base on `type_` name by default.
386
            only (str): Limit parameters to these listed in here, separated by ';'
387
            item_suffix (str): Enumeration Suffix.
388
        """
389
        name = name or snakecase(type_.__class__.__name__.removesuffix("Type"))
15✔
390
        if only:
15✔
391
            patfilter = namefilter(only)
×
392

393
            def filter_(item: StructItem):
×
394
                return patfilter(item.name)
×
395

396
            type_ = DescriptiveStructType(type_, filter_=filter_, enumitem_suffix=item_suffix)
×
397
        else:
398
            type_ = DescriptiveStructType(type_, enumitem_suffix=item_suffix)
15✔
399
        self.add_const(type_, name, exist_ok=exist_ok, title=type_.title, descr=type_.descr, comment=type_.comment)
15✔
400

401
    def add_port(
15✔
402
        self,
403
        type_: BaseType,
404
        name: str,
405
        direction: Direction | None = None,
406
        title: str | None = None,
407
        descr: str | None = None,
408
        comment: str | None = None,
409
        ifdef: str | None = None,
410
        ifdefs: Ifdefs | str | None = None,
411
        route: Routeables | None = None,
412
        clkrel: str | Port | BaseClkRel | None = None,
413
    ) -> Port | None:
414
        """
415
        Add Module Port (:any:`Port`).
416

417
        Args:
418
            type_: Type.
419
            name: Name.
420

421
        Keyword Args:
422
            direction: Signal Direction. Automatically detected if `name` ends with '_i', '_o', '_io'.
423
            title: Full Spoken Name.
424
            descr: Documentation Description.
425
            comment: Source Code Comment. Default is 'title'
426
            ifdef: IFDEF pragma. Obsolete.
427
            ifdefs: IFDEFs pragmas.
428
            route: Routes (iterable or string separated by ';')
429
            clkrel: Clock relation.
430
        """
431
        if ifdef:
15✔
432
            warnings.warn(
×
433
                "add_port(..., ifdef=...) is obsolete, please use ifdefs=", category=DeprecationWarning, stacklevel=1
434
            )
435
        doc = doc_from_type(type_, title, descr, comment)
15✔
436
        if direction is None:
15✔
437
            direction = Direction.from_name(name) or IN
15✔
438
        if clkrel is not None:
15✔
439
            clkrel = self._resolve_clkrel(clkrel)
15✔
440
        ifdefs = cast_ifdefs(ifdefs or ifdef)
15✔
441
        rifdefs = resolve_ifdefs(self.defines, ifdefs)
15✔
442
        if rifdefs is None:
15✔
443
            return None
15✔
444
        port = Port(type_, name, direction=direction, doc=doc, ifdefs=rifdefs, clkrel=clkrel)
15✔
445
        if self.__is_locked:
15✔
446
            raise LockError(f"{self}: Cannot add port {name!r}.")
15✔
447
        self.namespace[name] = port
15✔
448
        self.portssignals[name] = port
15✔
449
        self.ports[name] = port
15✔
450
        for routepath in parse_routepaths(route):
15✔
451
            self._router.add(RoutePath(expr=port), routepath)
15✔
452
        return port
15✔
453

454
    def _resolve_clkrel(self, clkrel: str | Port | BaseClkRel) -> BaseClkRel:
15✔
455
        if isinstance(clkrel, BaseClkRel):
15✔
456
            return clkrel
15✔
457
        if isinstance(clkrel, BaseSignal):
15✔
458
            return ClkRel(clk=clkrel)
×
459
        if isinstance(clkrel, str):
15✔
460
            port = self.ports[clkrel]
15✔
461
            return ClkRel(clk=port)
15✔
462
        raise ValueError(f"Invalid {clkrel=}")
×
463

464
    def add_signal(
15✔
465
        self,
466
        type_: BaseType,
467
        name: str,
468
        direction: Orientation = FWD,
469
        title: str | None = None,
470
        descr: str | None = None,
471
        comment: str | None = None,
472
        ifdef: str | None = None,
473
        ifdefs: Ifdefs | str | None = None,
474
        route: Routeables | None = None,
475
        clkrel: str | Port | BaseClkRel | None = None,
476
    ) -> Signal | None:
477
        """
478
        Add Module Internal Signal (:any:`Signal`).
479

480
        Args:
481
            type_: Type.
482
            name: Name.
483

484
        Keyword Args:
485
            direction: Signal Direction. Automatically detected if `name` ends with '_i', '_o', '_io'.
486
            title: Full Spoken Name.
487
            descr: Documentation Description.
488
            comment: Source Code Comment. Default is 'title'
489
            ifdef: IFDEF pragma. Obsolete.
490
            ifdefs: IFDEFs pragmas.
491
            route: Routes (iterable or string separated by ';')
492
            clkrel: Clock relation.
493
        """
494
        if ifdef:
15✔
495
            warnings.warn(
×
496
                "add_signal(..., ifdef=...) is obsolete, please use ifdefs=", category=DeprecationWarning, stacklevel=1
497
            )
498
        doc = doc_from_type(type_, title, descr, comment)
15✔
499
        if clkrel is not None:
15✔
500
            clkrel = self._resolve_clkrel(clkrel)
×
501
        ifdefs = cast_ifdefs(ifdefs or ifdef)
15✔
502
        rifdefs = resolve_ifdefs(self.defines, ifdefs)
15✔
503
        if rifdefs is None:
15✔
NEW
504
            return None
×
505
        signal = Signal(type_, name, direction=direction, doc=doc, ifdefs=rifdefs, clkrel=clkrel)
15✔
506
        if self.__is_locked:
15✔
507
            raise LockError(f"{self}: Cannot add signal {name!r}.")
15✔
508
        self.namespace[name] = signal
15✔
509
        self.portssignals[name] = signal
15✔
510
        for routepath in parse_routepaths(route):
15✔
511
            self._router.add(RoutePath(expr=signal), routepath)
×
512
        return signal
15✔
513

514
    def add_port_or_signal(
15✔
515
        self,
516
        type_: BaseType,
517
        name: str,
518
        direction: Direction | None = None,
519
        title: str | None = None,
520
        descr: str | None = None,
521
        comment: str | None = None,
522
        ifdef: str | None = None,
523
        ifdefs: Ifdefs | str | None = None,
524
        route: Routeables | None = None,
525
        clkrel: str | Port | BaseClkRel | None = None,
526
    ) -> BaseSignal | None:
527
        """
528
        Add Module Port (:any:`Port`) or Signal (:any:`Signal`) depending on name.
529

530
        Args:
531
            type_: Type.
532
            name: Name.
533

534
        Keyword Args:
535
            direction: Signal Direction. Automatically detected if `name` ends with '_i', '_o', '_io'.
536
            title: Full Spoken Name.
537
            descr: Documentation Description.
538
            comment: Source Code Comment. Default is 'title'
539
            ifdef: IFDEF pragma. Obsolete.
540
            ifdefs: IFDEFs pragmas.
541
            route: Routes (iterable or string separated by ';')
542
            clkrel: Clock relation.
543
        """
544
        if ifdef:
×
545
            warnings.warn(
×
546
                "add_port_or_signal(..., ifdef=...) is obsolete, please use ifdefs=",
547
                category=DeprecationWarning,
548
                stacklevel=1,
549
            )
550
        ifdefs = cast_ifdefs(ifdefs or ifdef)
×
551
        if direction is None:
×
552
            direction = Direction.from_name(name)
×
553
        if direction is None:
×
554
            return self.add_signal(
×
555
                type_,
556
                name,
557
                title=title,
558
                descr=descr,
559
                comment=comment,
560
                ifdefs=ifdefs,
561
                route=route,
562
                clkrel=clkrel,
563
            )
564
        return self.add_port(
×
565
            type_,
566
            name,
567
            direction=direction,
568
            title=title,
569
            descr=descr,
570
            comment=comment,
571
            ifdefs=ifdefs,
572
            route=route,
573
            clkrel=clkrel,
574
        )
575

576
    def set_parent(self, parent: "BaseMod") -> None:
15✔
577
        """
578
        Set Parent.
579

580
        Do not use this method, until you really really really know what you do.
581
        """
582
        self.__parents.append(parent)
15✔
583

584
    def assign(
15✔
585
        self,
586
        target: Parseable,
587
        source: Parseable | Note,
588
        cast: bool = False,
589
        overwrite: bool = False,
590
    ):
591
        """
592
        Assign `target` to `source`.
593

594
        The assignment is done **without** routing.
595

596
        Args:
597
            target: Target to be driven. Must be known within this module.
598
            source: Source driving target. Must be known within this module.
599

600
        Keyword Args:
601
            cast (bool): Cast. `False` by default.
602
            overwrite (bool): Overwrite existing assignment.
603
            filter_ (str, Callable): Target names or function to filter target identifiers.
604
        """
605
        if self.__is_locked:
15✔
606
            raise LockError(f"{self}: Cannot add assign '{source}' to '{target}'.")
15✔
607
        parser = self.parser
×
608
        assigntarget: BaseSignal = parser.parse(target, only=BaseSignal)  # type: ignore[assignment]
×
609
        assignsource: Source = parser.parse_note(source, only=Source)  # type: ignore[arg-type]
×
610
        self.assigns.set(assigntarget, assignsource, cast=cast, overwrite=overwrite)
×
611

612
    def add_inst(self, inst: "BaseMod") -> None:
15✔
613
        """
614
        Add Submodule `inst`.
615

616
        Args:
617
            inst: Instance.
618
        """
619
        if self.__is_locked:
15✔
620
            raise LockError(f"{self}: Cannot add instance '{inst}'.")
15✔
621
        inst.set_parent(self)
15✔
622
        self.insts.add(inst)  # type: ignore[arg-type]
15✔
623
        assigns = Assigns(targets=inst.ports, sources=self.namespace, drivers=Drivers(), inst=True)
15✔
624
        parser = ExprParser(namespace=inst.ports, context=str(inst))
15✔
625
        self.__instcons[inst.name] = assigns, parser
15✔
626

627
    def get_inst(self, inst_or_name: Union["BaseMod", str]) -> "BaseMod":
15✔
628
        """
629
        Get Module Instance.
630
        """
631
        if not isinstance(inst_or_name, str):
15✔
632
            try:
15✔
633
                return self.insts[inst_or_name.name]
15✔
634
            except KeyError:
15✔
635
                raise ValueError(f"{inst_or_name} is not a sub-module of {self}") from None
15✔
636
        inst = self
15✔
637
        for part in inst_or_name.split("/"):
15✔
638
            if part == UPWARDS:
15✔
639
                if inst.parent is None:
15✔
640
                    raise ValueError(f"{self}: {inst} has no parent.")
15✔
641
                inst = inst.parent
15✔
642
            else:
643
                try:
15✔
644
                    inst = inst.insts.get_dym(part)  # type: ignore[assignment]
15✔
645
                except ValueError as exc:
15✔
646
                    raise ValueError(f"{self} has no sub-module {exc}") from None
15✔
647
        return inst
15✔
648

649
    def set_instcon(
15✔
650
        self,
651
        inst: Union["BaseMod", str],
652
        port: Parseable,
653
        expr: Parseable,
654
        cast: bool = False,
655
        overwrite: bool = False,
656
    ):
657
        """
658
        Connect `port` of `inst` to `expr` without routing.
659

660
        The assignment is done **without** routing.
661

662
        Args:
663
            inst: Module Instance
664
            port: Port to be connected. Must be known within module instance.
665
            expr: Expression. Must be known within this module.
666

667
        Keyword Args:
668
            cast: Cast. `False` by default.
669
            overwrite: Overwrite existing assignment.
670
        """
671
        if self.__is_locked:
15✔
672
            raise LockError(f"{self}: Cannot connect '{port}' of'{inst}' to '{expr}'.")
15✔
673
        mod: BaseMod = self.get_inst(inst)
15✔
674
        assigns, parser = self.__instcons[mod.name]
15✔
675
        assigntarget: BaseSignal = parser.parse(port, only=BaseSignal)  # type: ignore[assignment]
15✔
676
        assignsource: Source = self.parser.parse_note(expr, only=Source)  # type: ignore[arg-type]
15✔
677
        assigns.set(assigntarget, assignsource, cast=cast, overwrite=overwrite)
15✔
678

679
    def get_instcons(self, inst: Union["BaseMod", str]) -> Assigns:
15✔
680
        """Retrieve All Instance Connections Of `inst`."""
681
        mod: BaseMod = self.get_inst(inst)
15✔
682
        return self.__instcons[mod.name][0]
15✔
683

684
    def add_flipflop(
15✔
685
        self,
686
        type_: BaseType,
687
        name: str,
688
        clk: Parseable,
689
        rst_an: Parseable,
690
        nxt: Parseable | None = None,
691
        rst: Parseable | None = None,
692
        ena: Parseable | None = None,
693
        route: Routeables | None = None,
694
    ) -> Signal:
695
        """
696
        Add Module Internal Flip-Flop.
697

698
        Args:
699
            type_: Type.
700
            name: Name.
701
            clk: Clock. Module Clock by default.
702
            rst_an: Reset. Module Reset by default.
703

704
        Keyword Args:
705
            nxt: Next Value. Basename of `name` with _nxt_s by default.
706
            rst: Synchronous Reset.
707
            ena: Enable Condition.
708
            route: Routing of flip-flop output.
709
        """
710
        parser = self.parser
15✔
711
        if self.__is_locked:
15✔
712
            raise LockError(f"{self}: Cannot add flipflop {name!r}.")
15✔
713
        out = self.add_signal(type_, name)
15✔
714
        # clk
715
        clk_sig: BaseSignal = parser.parse(clk, only=BaseSignal)  # type: ignore[assignment]
15✔
716
        # rst_an
717
        rst_an_sig: BaseSignal = parser.parse(rst_an, only=BaseSignal)  # type: ignore[assignment]
15✔
718
        # nxt
719
        if nxt is None:
15✔
720
            nxt = self.add_signal(type_, f"{out.basename}_nxt_s")
15✔
721
        else:
722
            nxt = parser.parse(nxt)
15✔
723
            # TODO: check connectable of nxt and out?
724
        # rst
725
        rst_expr: BoolOp | None = cast_booltype(parser.parse(rst)) if rst is not None else None
15✔
726
        # ena
727
        ena_expr: BoolOp | None = cast_booltype(parser.parse(ena)) if ena is not None else None
15✔
728
        # flipflop
729
        flipflop = self._create_flipflop(clk_sig, rst_an_sig, rst_expr, ena_expr)
15✔
730
        flipflop.set(out, nxt)
15✔
731
        # route
732
        for routepath in parse_routepaths(route):
15✔
733
            self._router.add(RoutePath(expr=out), routepath)
×
734
        return out
15✔
735

736
    def _create_flipflop(
15✔
737
        self,
738
        clk: BaseSignal,
739
        rst_an: BaseSignal,
740
        rst: BoolOp | None = None,
741
        ena: BoolOp | None = None,
742
    ) -> FlipFlop:
743
        flipflops = self.__flipflops
15✔
744
        key = hash((clk, rst_an, rst, ena))
15✔
745
        try:
15✔
746
            return flipflops[key]
15✔
747
        except KeyError:
15✔
748
            pass
15✔
749
        flipflops[key] = flipflop = FlipFlop(
15✔
750
            clk=clk,
751
            rst_an=rst_an,
752
            rst=rst,
753
            ena=ena,
754
            targets=self.portssignals,
755
            sources=self.namespace,
756
            drivers=self.drivers,
757
        )
758
        return flipflop
15✔
759

760
    @property
15✔
761
    def flipflops(self) -> tuple[FlipFlop, ...]:
15✔
762
        """
763
        Flip Flops.
764
        """
765
        return tuple(self.__flipflops.values())
15✔
766

767
    def add_mux(
15✔
768
        self,
769
        name,
770
        title: str | None = None,
771
        descr: str | None = None,
772
        comment: str | None = None,
773
    ) -> Mux:
774
        """
775
        Add Multiplexer with `name` And Return It For Filling.
776

777
        Args:
778
            name (str): Name.
779

780
        Keyword Args:
781
            title (str): Full Spoken Name.
782
            descr (str): Documentation Description.
783
            comment (str): Source Code Comment.
784

785
        See :any:`Mux.set()` how to fill the multiplexer and the example above.
786
        """
787
        if self.__is_locked:
15✔
788
            raise LockError(f"{self}: Cannot add mux {name!r}.")
15✔
789
        doc = Doc(title=title, descr=descr, comment=comment)
15✔
790
        self.__muxes[name] = mux = Mux(
15✔
791
            name=name,
792
            targets=self.portssignals,
793
            namespace=self.namespace,
794
            # drivers=self.drivers,
795
            parser=self.parser,
796
            doc=doc,
797
        )
798
        return mux
15✔
799

800
    @property
15✔
801
    def muxes(self) -> tuple[Mux, ...]:
15✔
802
        """
803
        Iterate over all Multiplexer.
804
        """
805
        return tuple(self.__muxes.values())
15✔
806

807
    def get_mux(self, mux: Mux | str) -> Mux:
15✔
808
        """Get Multiplexer."""
809
        if not isinstance(mux, str):
15✔
810
            return self.__muxes.get_dym(mux.name)  # type: ignore[return-value]
15✔
811
        return self.__muxes.get_dym(mux)  # type: ignore[return-value]
15✔
812

813
    @property
15✔
814
    def is_locked(self) -> bool:
15✔
815
        """
816
        Return If Module Is Already Completed And Locked For Modification.
817

818
        Locking is done by the build process **automatically** and **MUST NOT** be done earlier or later.
819
        Use a different module type or enumeration or struct type, if you have issues with locking.
820
        """
821
        return self.__is_locked
15✔
822

823
    def lock(self):
15✔
824
        """
825
        Lock.
826

827
        Locking is done via this method by the build process **automatically** and **MUST NOT** be done earlier or
828
        later. Use a different module type or enumeration or struct type, if you have issues with locking.
829
        """
830
        if self.__is_locked:
15✔
831
            raise LockError(f"{self} is already locked. Cannot lock again.")
15✔
832
        for _, obj in self:
15✔
833
            if isinstance(obj, Namespace):
15✔
834
                obj.lock()
15✔
835
        self.__is_locked = True
15✔
836

837
    def check_lock(self):
15✔
838
        """Check if module is locked for modifications."""
839
        if self.__is_locked:
×
840
            raise LockError(f"{self}: Is already locked for modifications.")
×
841

842
    def con(self, port: Routeables, source: Routeables, on_error: RoutingError = "error"):
15✔
843
        """Connect `port` to `dest`."""
844
        parents = self.__parents
15✔
845
        if not parents:
15✔
846
            raise TypeError(f"{self} is top module. Connections cannot be made.")
×
847
        router = parents[-1]._router
15✔
848
        for subtarget in parse_routepaths(port, basepath=self.name):
15✔
849
            for subsource in parse_routepaths(source):
15✔
850
                router.add(subtarget, subsource, on_error=on_error)
15✔
851

852
    def route(self, target: Routeables, source: Routeables, on_error: RoutingError = "error"):
15✔
853
        """Route `source` to `target` within the actual module."""
854
        router = self._router
×
855
        for subtarget in parse_routepaths(target):
×
856
            for subsource in parse_routepaths(source):
×
857
                router.add(subtarget, subsource, on_error=on_error)
×
858

859
    def __str__(self):
15✔
860
        modref = self.get_modref()
15✔
861
        defines = ""
15✔
862
        if self.defines:
15✔
863
            definesdict = {define.name: define.value for define in self.defines}
15✔
864
            defines = f" defines={definesdict!r}"
15✔
865
        return f"<{modref}(inst={self.inst!r}, libname={self.libname!r}, modname={self.modname!r}{defines})>"
15✔
866

867
    def __repr__(self):
15✔
868
        modref = self.get_modref()
15✔
869
        defines = ""
15✔
870
        if self.defines:
15✔
NEW
871
            definesdict = {define.name: define.value for define in self.defines}
×
NEW
872
            defines = f" defines={definesdict!r}"
×
873
        return f"<{modref}(inst={self.inst!r}, libname={self.libname!r}, modname={self.modname!r}{defines})>"
15✔
874

875
    def get_overview(self) -> str:
15✔
876
        """
877
        Return Brief Module Overview.
878

879
        This Module Overview is intended to be overwritten by the user.
880
        """
881
        return ""
×
882

883
    def get_info(self, sub: bool = False) -> str:
15✔
884
        """Module Information."""
885
        header = f"## `{self.libname}.{self.modname}` (`{self.get_modref()}`)"
×
886
        parts = [
×
887
            header,
888
            self._get_ident_info("Parameters", self.params),
889
            self._get_ident_info("Ports", self.ports),
890
        ]
891
        if sub:
×
892
            parts.append(self._get_sub_info())
×
893
        return "\n\n".join(parts)
×
894

895
    def _get_ident_info(self, title: str, idents: Idents):
15✔
896
        def entry(level, ident):
×
897
            pre = "  " * level
×
898
            dinfo = f" ({ident.direction})" if ident.direction else ""
×
899
            return (
×
900
                f"{pre}{ident.name}{dinfo}",
901
                f"{pre}{ident.type_}",
902
            )
903

904
        parts = [
×
905
            f"### {title}",
906
            "",
907
        ]
908
        if idents:
×
909
            data = [("Name ", "Type"), ("----", "----")]
×
910
            data += [entry(level, ident) for level, ident in idents.leveliter()]
×
911
            parts.append(align(data, seps=(" | ", " |"), sepfirst="| "))
×
912
        else:
913
            parts.append("-")
×
914
        return "\n".join(parts)
×
915

916
    def _get_sub_info(self) -> str:
15✔
917
        parts = [
×
918
            "### Submodules",
919
            "",
920
        ]
921
        if self.insts:
×
922
            data = [("Name", "Module"), ("----", "------")]
×
923
            data += [(f"`{inst.name}`", f"`{inst.libname}.{inst.modname}`") for inst in self.insts]
×
924
            parts.append(align(data, seps=(" | ", " |"), sepfirst="| "))
×
925
        else:
926
            parts.append("-")
×
927
        return "\n".join(parts)
×
928

929

930
class Router(Object):
15✔
931
    """The One And Only Router."""
932

933
    mod: BaseMod
12✔
934
    __routes: list[tuple[RoutePath, RoutePath, RoutingError]] = PrivateField(default_factory=list)
15✔
935

936
    def add(self, tpath: RoutePath, spath: RoutePath, on_error: RoutingError = "error") -> None:
15✔
937
        """Add route from `source` to `tpath`."""
938
        LOGGER.debug("%s: router: add '%s' to '%s'", self.mod, spath, tpath)
15✔
939
        self.__routes.append(self._create(tpath, spath, on_error))
15✔
940

941
    def flush(self) -> None:
15✔
942
        """Create Pending Routes."""
943
        for tpath, spath, on_error in self.__routes:
15✔
944
            tpathc, spathc, on_errorc = self._create(tpath, spath, on_error)
15✔
945
            try:
15✔
946
                self._route(tpathc, spathc)
15✔
947
            except Exception as exc:
15✔
948
                if on_errorc == "ignore":
15✔
949
                    LOGGER.info("Ignored: %s", exc)
15✔
950
                elif on_errorc == "warn":
15✔
951
                    LOGGER.warning(exc)
15✔
952
                else:
953
                    raise
15✔
954
        self.__routes.clear()
15✔
955

956
    def _create(
15✔
957
        self, tpath: RoutePath, spath: RoutePath, on_error: RoutingError
958
    ) -> tuple[RoutePath, RoutePath, RoutingError]:
959
        if tpath.create:
15✔
960
            if self.__create(spath, tpath):
15✔
961
                tpath = tpath.new(create=False)
15✔
962
        elif spath.create:
15✔
963
            if self.__create(tpath, spath):
15✔
964
                spath = spath.new(create=False)
15✔
965
        return tpath, spath, on_error
15✔
966

967
    @no_type_check  # TODO: fix types
15✔
968
    def __create(self, rpath: RoutePath, cpath: RoutePath) -> bool:
15✔
969
        """Create `cpath` based on `rpath`."""
970
        assert not rpath.create
15✔
971
        assert cpath.create
15✔
972
        mod = self.mod
15✔
973
        # Resolve reference path
974
        try:
15✔
975
            rmod = mod.get_inst(rpath.path) if rpath.path else mod
15✔
976
            rident: Ident = rmod.parser.parse(rpath.expr, only=Ident)  # type: ignore[assignment]
15✔
977
            cmod = mod.get_inst(cpath.path) if cpath.path else mod
15✔
978
        except (ValueError, NameError, KeyError):
15✔
979
            return False
15✔
980
        self.__create_port_or_signal(cmod, rident, cpath.expr)
15✔
981
        return True
15✔
982

983
    @no_type_check  # TODO: fix types
15✔
984
    def _route(self, tpath: RoutePath, spath: RoutePath):  # noqa: C901, PLR0912, PLR0915
15✔
985
        mod = self.mod
15✔
986
        LOGGER.debug("%s router: routing %r to %r", mod, spath, tpath)
15✔
987
        # Referenced modules
988
        tmod = mod.get_inst(tpath.path) if tpath.path else mod
15✔
989
        smod = mod.get_inst(spath.path) if spath.path else mod
15✔
990
        # Referenced expression/signal
991
        texpr = tmod.parser.parse(tpath.expr) if not isinstance(tpath.expr, Note) else tpath.expr
15✔
992
        sexpr = smod.parser.parse(spath.expr) if not isinstance(spath.expr, Note) else spath.expr
15✔
993
        tident = None if not isinstance(texpr, Ident) else texpr
15✔
994
        sident = None if not isinstance(sexpr, Ident) else sexpr
15✔
995
        tparts = tpath.parts
15✔
996
        sparts = spath.parts
15✔
997
        # One of the both sides need to exist
998
        rident = tident or sident
15✔
999
        assert rident is not None
15✔
1000
        direction = (
15✔
1001
            tident.direction * sident.direction
1002
            if tident and tident.direction is not None and sident and sident.direction is not None
1003
            else rident.direction
1004
        )
1005

1006
        cast = _merge_cast(tpath.cast, spath.cast)
15✔
1007
        assert len(tparts) in (0, 1)
15✔
1008
        assert len(sparts) in (0, 1)
15✔
1009
        if tparts:
15✔
1010
            # target is submodule
1011
            assert tparts[0] != UPWARDS
15✔
1012
            if sparts:
15✔
1013
                assert sparts[0] != UPWARDS
15✔
1014
                # source and target are submodules
1015
                tcon = None if tident is None else mod.get_instcons(tmod).get(tident)
15✔
1016
                scon = None if sident is None else mod.get_instcons(smod).get(sident)
15✔
1017
                if tcon is None and scon is None:
15✔
1018
                    modname = split_prefix(tmod.name)[1]
15✔
1019
                    name = join_names(modname, rident.name, "s")
15✔
1020
                    rsig = mod.add_signal(rident.type_, name, ifdefs=rident.ifdefs, direction=direction)
15✔
1021
                    Router.__routesubmod(mod, tmod, rident, texpr, rsig, tname=tpath.expr, cast=tpath.cast)
15✔
1022
                    Router.__routesubmod(mod, smod, rident, sexpr, rsig, tname=spath.expr, cast=spath.cast)
15✔
1023
                elif tcon is None:
×
1024
                    Router.__routesubmod(mod, tmod, rident, texpr, scon, tname=tpath.expr, cast=tpath.cast)
×
1025
                elif scon is None:
×
1026
                    Router.__routesubmod(mod, smod, rident, sexpr, tcon, tname=spath.expr, cast=spath.cast)
×
1027
                else:
1028
                    mod.assign(tcon, scon, cast=cast)
×
1029
            else:
1030
                tcon = None if tident is None else mod.get_instcons(tmod).get(tident)
15✔
1031
                if tcon is None:
15✔
1032
                    Router.__routesubmod(mod, tmod, rident, texpr, sexpr, tpath.expr, spath.expr, cast=cast)
15✔
1033
                else:
1034
                    if sexpr is None:
×
1035
                        sexpr = Router.__create_port_or_signal(mod, rident, spath.expr)
×
1036
                    mod.assign(tcon, sexpr, cast=cast)
×
1037
        elif sparts:
15✔
1038
            scon = None if sident is None else mod.get_instcons(smod).get(sident)
15✔
1039
            assert sparts[0] != UPWARDS
15✔
1040
            if scon is None:
15✔
1041
                Router.__routesubmod(mod, smod, rident, sexpr, texpr, spath.expr, tpath.expr, cast=cast)
15✔
1042
            else:
1043
                if texpr is None:
×
1044
                    texpr = Router.__create_port_or_signal(mod, rident, tpath.expr)
×
1045
                mod.assign(scon, texpr, cast=cast)
×
1046
        else:
1047
            # connect signals of `mod`
1048
            if texpr is None:
×
1049
                texpr = Router.__create_port_or_signal(mod, rident, tpath.expr)
×
1050
            if sexpr is None:
×
1051
                sexpr = Router.__create_port_or_signal(mod, rident, spath.expr)
×
1052
            mod.assign(texpr, sexpr, cast=cast)
×
1053

1054
    @staticmethod
15✔
1055
    def __routesubmod(
15✔
1056
        mod: BaseMod, submod: BaseMod, rident: Ident, texpr, sexpr, tname=None, sname=None, cast=False
1057
    ) -> Expr:
1058
        if texpr is None:
15✔
1059
            assert tname is not None
×
1060
            assert rident is not None and rident.type_
×
1061
            texpr = submod.add_port(rident.type_, tname, ifdefs=rident.ifdefs)
×
1062
        if sexpr is None:
15✔
1063
            sexpr = Router.__create_port_or_signal(mod, rident, sname)
×
1064
        if not isinstance(texpr, Port):
15✔
1065
            raise ValueError(f"Cannot route {type(texpr)} to module instance {submod}")
×
1066
        try:
15✔
1067
            mod.set_instcon(submod, texpr, sexpr, cast=cast)
15✔
1068
        except TypeError as err:
15✔
1069
            raise TypeError(f"{mod}: {err}") from None
15✔
1070
        return sexpr
15✔
1071

1072
    @staticmethod
15✔
1073
    def __create_port_or_signal(mod: BaseMod, rident: Ident, name: str) -> BaseSignal:
15✔
1074
        assert isinstance(rident, Ident)
15✔
1075
        assert name is not None
15✔
1076
        assert rident is not None and rident.type_ is not None, (mod, name)
15✔
1077
        type_ = rident.type_
15✔
1078
        direction = Direction.from_name(name)
15✔
1079
        signal: BaseSignal
1080
        if direction is not None:
15✔
1081
            signal = mod.add_port(type_, name, ifdefs=rident.ifdefs, direction=direction)
15✔
1082
        else:
1083
            signal = mod.add_signal(type_, name, ifdefs=rident.ifdefs)
15✔
1084
        LOGGER.debug("%s: router: creating %r", mod, signal)
15✔
1085
        return signal
15✔
1086

1087

1088
def _merge_cast(one, other):
15✔
1089
    # TODO: get rid of this.
1090
    if one or other:
15✔
1091
        return True
×
1092
    if one is None or other is None:
15✔
1093
        return None
×
1094
    return False
15✔
1095

1096

1097
ModCls: TypeAlias = type[BaseMod]
15✔
1098
ModClss: TypeAlias = set[type[BaseMod]]
15✔
1099

1100

1101
def get_modbaseclss(cls):
15✔
1102
    """Get Module Base Classes."""
1103
    clss = []
15✔
1104
    for basecls in getmro(cls):
15✔
1105
        if basecls is BaseMod:
15✔
1106
            break
15✔
1107
        clss.append(basecls)
15✔
1108
    return clss
15✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc