Coverage for pyVHDLModel/Namespace.py: 95%
190 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-25 10:05 +0000
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-25 10:05 +0000
1# ==================================================================================================================== #
2# __ ___ _ ____ _ __ __ _ _ #
3# _ __ _ \ \ / / | | | _ \| | | \/ | ___ __| | ___| | #
4# | '_ \| | | \ \ / /| |_| | | | | | | |\/| |/ _ \ / _` |/ _ \ | #
5# | |_) | |_| |\ V / | _ | |_| | |___| | | | (_) | (_| | __/ | #
6# | .__/ \__, | \_/ |_| |_|____/|_____|_| |_|\___/ \__,_|\___|_| #
7# |_| |___/ #
8# ==================================================================================================================== #
9# Authors: #
10# Patrick Lehmann #
11# #
12# License: #
13# ==================================================================================================================== #
14# Copyright 2017-2026 Patrick Lehmann - Boetzingen, Germany #
15# Copyright 2016-2017 Patrick Lehmann - Dresden, Germany #
16# #
17# Licensed under the Apache License, Version 2.0 (the "License"); #
18# you may not use this file except in compliance with the License. #
19# You may obtain a copy of the License at #
20# #
21# http://www.apache.org/licenses/LICENSE-2.0 #
22# #
23# Unless required by applicable law or agreed to in writing, software #
24# distributed under the License is distributed on an "AS IS" BASIS, #
25# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
26# See the License for the specific language governing permissions and #
27# limitations under the License. #
28# #
29# SPDX-License-Identifier: Apache-2.0 #
30# ==================================================================================================================== #
31#
32"""
33This module contains parts of an abstract document language model for VHDL.
35A helper class to implement namespaces and scopes.
36"""
37from __future__ import annotations
39from typing import TYPE_CHECKING, TypeVar, Generic, Optional as Nullable
41from pyTooling.Common import getFullyQualifiedName
42from pyTooling.Decorators import readonly
43from pyTooling.Warning import WarningCollector
45from pyVHDLModel.Base import identifiersOf, normalizedIdentifiersOf
46from pyVHDLModel.Object import Obj, Signal, Constant, Variable
47from pyVHDLModel.Symbol import ComponentInstantiationSymbol, Symbol, PossibleReference
48from pyVHDLModel.Exception import DuplicateDeclarationWarning, NotInNamespaceError
49if TYPE_CHECKING: 49 ↛ 50line 49 didn't jump to line 50 because the condition on line 49 was never true
50 from pyVHDLModel.Type import BaseType
52K = TypeVar("K")
53O = TypeVar("O")
56class ExtendedKeyError(KeyError):
57 """
58 A :exc:`KeyError` reporting which namespaces were searched.
60 Raised when a name cannot be resolved. Besides the key (:data:`key`), it carries every namespace
61 visited while walking outwards (:data:`searchedNamespaces`).
62 """
63 key: str #: The key that was not found.
64 searchedNamespaces: tuple[Namespace, ...] #: The namespaces that were searched for the key.
66 def __init__(self, key: str, searchedNamespaces: tuple[Namespace, ...], message: str) -> None:
67 """
68 Initializes an extended key error.
70 :param key: The key that was not found.
71 :param searchedNamespaces: The namespaces that were searched for the key.
72 :param message: The error message.
73 """
74 super().__init__(message)
76 self.key = key
77 self.searchedNamespaces = searchedNamespaces
80class Namespace(Generic[K, O]):
81 """
82 Represents a namespace: the declared items visible in one declarative region.
84 Namespaces nest, so a lookup that misses locally continues in the parent namespace
85 (:data:`ParentNamespace`). That is what makes an entity's ports visible inside its architecture,
86 and lets a process variable hide an outer signal.
88 .. seealso::
90 * :class:`Concurrent declaration region <pyVHDLModel.Regions.ConcurrentDeclarationRegionMixin>`
91 * :class:`Sequential declaration region <pyVHDLModel.Regions.SequentialDeclarationRegionMixin>`
92 """
93 _name: str #: The namespace's name.
94 _parentNamespace: Nullable[Namespace] #: Reference to the enclosing namespace, ``None`` if outermost.
95 _subNamespaces: list[Namespace] #: List of all nested namespaces, in the order they were linked.
96 _elements: dict[K, O] #: All elements declared in this namespace, indexed by name.
97 _imports: dict[K, O] #: All elements imported into this namespace, indexed by name.
98 _sharesRegionWithParent: bool #: ``True`` if the parent namespace is the same declarative region.
100 def __init__(
101 self,
102 name: str,
103 parentNamespace: Nullable[Namespace] = None,
104 sharesRegionWithParent: bool = False
105 ) -> None:
106 """
107 Initializes a namespace.
109 :param name: The namespace's name.
110 :param parentNamespace: Optional, reference to the enclosing namespace, or ``None`` for the outermost one.
111 Default: ``None``. |br|
112 This namespace is appended to the parent namespace's sub-namespaces.
113 :param sharesRegionWithParent: Optional, ``True`` if the parent namespace is the same declarative region.
114 Default: ``False``.
115 :raises TypeError: If parameter 'parentNamespace' is not of type :class:`Namespace`.
116 """
117 self._name = name
118 self._parentNamespace = None
119 self._subNamespaces = []
120 self._elements = {}
121 self._imports = {}
122 self._sharesRegionWithParent = sharesRegionWithParent
124 if parentNamespace is not None:
125 self.ParentNamespace = parentNamespace
127 @readonly
128 def Name(self) -> str:
129 """
130 Read-only property to access the name (:attr:`_name`).
132 :returns: The name.
133 """
134 return self._name
136 @property
137 def ParentNamespace(self) -> Nullable[Namespace]:
138 """
139 Property to access the parent namespace (:attr:`_parentNamespace`).
141 Assigning a namespace links both sides: this namespace leaves the sub-namespaces (:attr:`SubNamespaces`) of its
142 previous parent namespace and is appended to the sub-namespaces of the new one. Assigning ``None`` unlinks this
143 namespace from its parent namespace. Assigning the current parent namespace changes nothing.
145 :returns: The parent namespace, or ``None`` if this namespace is the outermost one.
146 :raises TypeError: If the assigned value is not of type :class:`Namespace`.
147 """
148 return self._parentNamespace
150 @ParentNamespace.setter
151 def ParentNamespace(self, parentNamespace: Nullable[Namespace]) -> None:
152 if parentNamespace is not None and not isinstance(parentNamespace, Namespace):
153 ex = TypeError("Parameter 'parentNamespace' is not of type 'Namespace'.")
154 ex.add_note(f"Got type '{getFullyQualifiedName(parentNamespace)}'.")
155 raise ex
157 if parentNamespace is self._parentNamespace:
158 return
160 if self._parentNamespace is not None:
161 self._parentNamespace._subNamespaces.remove(self)
163 self._parentNamespace = parentNamespace
164 if parentNamespace is not None:
165 parentNamespace._subNamespaces.append(self)
167 @readonly
168 def SharesRegionWithParent(self) -> bool:
169 """
170 Read-only property to access whether this namespace continues its parent's declarative region
171 (:attr:`_sharesRegionWithParent`).
173 .. hint::
175 An entity and its architecture form one VHDL declarative region, as do a package and its body,
176 even though each owns a namespace. A duplicate declaration is reported across such a link, while
177 a genuinely nested region - a process, a block - hides instead.
179 :returns: ``True`` if the parent namespace is the same declarative region.
180 """
181 return self._sharesRegionWithParent
183 @readonly
184 def SubNamespaces(self) -> list[Namespace]:
185 """
186 Read-only property to access the sub namespaces (:attr:`_subNamespaces`).
188 A sub-namespace is linked by assigning this namespace to its :attr:`ParentNamespace`. Several sub-namespaces may
189 share a name, e.g. ``None`` for unlabeled processes.
191 :returns: List of sub namespaces, in the order they were linked.
192 """
193 return self._subNamespaces
195 def AddElement(self, normalizedIdentifier: K, element: O, overloadable: bool = False) -> None:
196 """
197 Add a declared item to this namespace, reporting a duplicate declaration.
199 VHDL rejects two declarations sharing an identifier in one declarative region. A region can span
200 more than one namespace - an entity and its architecture form one, as do a package and its body -
201 so enclosing namespaces are searched too, but only while they share this one's region.
203 Overloadable declarations are exempt: several subprograms may share a name as long as their
204 signatures differ. Signatures are not compared yet, so two subprograms sharing a name are always
205 accepted (see the overload-resolution finding).
207 An item imported into a namespace (e.g. by a use clause) is no declaration of that namespace's region: it's kept
208 apart (:meth:`AddImport`), so it never clashes with a declaration, and a lookup finds the declaration first.
210 :param normalizedIdentifier: The normalized (lower case) identifier being declared.
211 :param element: The declared item.
212 :param overloadable: ``True`` if this declaration may legally share its name.
213 :raises UnhandledCriticalWarningError: If the identifier is already declared in the declarative region and no
214 :class:`~pyTooling.Warning.WarningCollector` collects the
215 :exc:`~pyVHDLModel.Exception.DuplicateDeclarationWarning`.
216 """
217 from pyVHDLModel.Subprogram import Function, Procedure
219 namespace = self
220 while namespace is not None:
221 existing = namespace._elements.get(normalizedIdentifier)
222 # `existing is element` means the region is being re-indexed, not that the name is declared twice.
223 isReindex = existing is element
224 isOverload = overloadable and isinstance(existing, (Function, Procedure))
225 if existing is not None and not isReindex and not isOverload:
226 WarningCollector.Raise(DuplicateDeclarationWarning(
227 f"Identifier '{normalizedIdentifier}' is already used for a declaration in '{namespace._name}'.",
228 identifier=self._IdentifierOf(element, normalizedIdentifier),
229 namespace=namespace,
230 element=element,
231 existing=existing
232 ))
233 break
235 namespace = namespace._parentNamespace if namespace._sharesRegionWithParent else None
237 self._elements[normalizedIdentifier] = element
239 def RemoveElement(self, normalizedIdentifier: K, element: O) -> None:
240 """
241 Remove a declared item from this namespace.
243 :param normalizedIdentifier: The normalized (lower case) identifier of the declared item.
244 :param element: The declared item.
245 :raises NotInNamespaceError: If the identifier isn't declared in this namespace.
246 :raises NotInNamespaceError: If the identifier refers to another element, e.g. a duplicate declaration.
247 """
248 if (existing := self._elements.get(normalizedIdentifier)) is None:
249 raise NotInNamespaceError(
250 f"Namespace '{self._name}' has no element '{normalizedIdentifier}'.",
251 identifier=self._IdentifierOf(element, normalizedIdentifier),
252 namespace=self
253 )
254 elif existing is not element:
255 ex = NotInNamespaceError(
256 f"Namespace '{self._name}' has another element for '{normalizedIdentifier}'.",
257 identifier=self._IdentifierOf(element, normalizedIdentifier),
258 namespace=self
259 )
260 ex.add_note(f"Found element of type '{getFullyQualifiedName(existing)}'.")
261 raise ex
263 del self._elements[normalizedIdentifier]
265 def AddImport(self, normalizedIdentifier: K, element: O) -> None:
266 """
267 Add an item imported by a use clause to this namespace.
269 An imported item is declared elsewhere, so it's kept apart from the declarations (:meth:`AddElement`): it never
270 clashes with a declaration, and a lookup finds a declaration of the same identifier first. Importing another item
271 for the same identifier replaces the imported item.
273 :param normalizedIdentifier: The normalized (lower case) identifier of the imported item.
274 :param element: The imported item, e.g. a package or a component.
275 """
276 self._imports[normalizedIdentifier] = element
278 def RemoveImport(self, normalizedIdentifier: K, element: O) -> None:
279 """
280 Remove an imported item from this namespace.
282 Declarations of the same identifier are kept.
284 :param normalizedIdentifier: The normalized (lower case) identifier of the imported item.
285 :param element: The imported item.
286 :raises NotInNamespaceError: If the identifier isn't imported into this namespace.
287 :raises NotInNamespaceError: If the identifier refers to another imported item.
288 """
289 if (existing := self._imports.get(normalizedIdentifier)) is None:
290 raise NotInNamespaceError(
291 f"Namespace '{self._name}' has no import '{normalizedIdentifier}'.",
292 identifier=self._IdentifierOf(element, normalizedIdentifier),
293 namespace=self
294 )
295 elif existing is not element:
296 ex = NotInNamespaceError(
297 f"Namespace '{self._name}' has another import for '{normalizedIdentifier}'.",
298 identifier=self._IdentifierOf(element, normalizedIdentifier),
299 namespace=self
300 )
301 ex.add_note(f"Found element of type '{getFullyQualifiedName(existing)}'.")
302 raise ex
304 del self._imports[normalizedIdentifier]
306 def ClearImports(self) -> None:
307 """
308 Remove all imported items from this namespace.
310 The declarations (:meth:`Elements`) are kept.
311 """
312 self._imports.clear()
314 @staticmethod
315 def _IdentifierOf(element: O, normalizedIdentifier: K) -> str:
316 """
317 Return the identifier of an element as declared, for its normalized identifier.
319 :param element: The named element.
320 :param normalizedIdentifier: One of the element's normalized (lower case) identifiers.
321 :returns: The identifier in its declared spelling.
322 """
323 return next(
324 identifier
325 for identifier, normalized in zip(identifiersOf(element), normalizedIdentifiersOf(element))
326 if normalized == normalizedIdentifier
327 )
329 def Elements(self) -> dict[K, O]:
330 """
331 Return the elements declared in this namespace (:attr:`_elements`).
333 :returns: Dictionary of declared elements, indexed by normalized identifier.
334 """
335 return self._elements
337 def Imports(self) -> dict[K, O]:
338 """
339 Return the elements imported into this namespace (:attr:`_imports`).
341 :returns: Dictionary of imported elements, indexed by normalized identifier.
342 """
343 return self._imports
345 def _LookupElement(self, normalizedIdentifier: K) -> O:
346 """
347 Return the element of an identifier in this namespace: the declared element, else the imported element.
349 :param normalizedIdentifier: The normalized (lower case) identifier.
350 :returns: The declared or imported element.
351 :raises KeyError: If the identifier is neither declared nor imported in this namespace.
352 """
353 if (element := self._elements.get(normalizedIdentifier)) is not None:
354 return element
356 return self._imports[normalizedIdentifier]
358 def FindComponent(self, componentSymbol: ComponentInstantiationSymbol) -> Component:
359 from pyVHDLModel.DesignUnit import Component
361 try:
362 element = self._LookupElement(componentSymbol._name._normalizedIdentifier)
363 if isinstance(element, Component):
364 return element
365 else:
366 ex = TypeError(f"Found element '{componentSymbol._name._identifier}', but it is not a component.")
367 ex.add_note(f"Got type '{getFullyQualifiedName(element)}'.")
368 raise ex
369 except KeyError:
370 key = componentSymbol._name._identifier
372 if (parentNamespace := self._parentNamespace) is None:
373 raise ExtendedKeyError(key, (self, ), f"Component '{key}' not found in '{self._name}'.")
375 try:
376 return parentNamespace.FindComponent(componentSymbol)
377 except ExtendedKeyError as ex:
378 searchedNamespaces = (self, *ex.searchedNamespaces)
379 raise ExtendedKeyError(key, searchedNamespaces, f"Component '{key}' not found in: {', '.join(ns._name for ns in searchedNamespaces)}.") from ex
381 def FindSubtype(self, subtypeSymbol: Symbol) -> BaseType:
382 from pyVHDLModel.Type import Subtype, FullType
384 try:
385 element = self._LookupElement(subtypeSymbol._name._normalizedIdentifier)
386 if isinstance(element, Subtype):
387 if PossibleReference.Subtype in subtypeSymbol._possibleReferences:
388 return element
389 else:
390 ex = TypeError(f"Found subtype '{subtypeSymbol._name._identifier}', but it was not expected.")
391 ex.add_note(f"Got type '{getFullyQualifiedName(element)}'.")
392 ex.add_note(f"Expected one of: {subtypeSymbol._possibleReferences}.")
393 raise ex
394 elif isinstance(element, FullType):
395 if PossibleReference.Type in subtypeSymbol._possibleReferences:
396 return element
397 else:
398 ex = TypeError(f"Found type '{subtypeSymbol._name._identifier}', but it was not expected.")
399 ex.add_note(f"Got type '{getFullyQualifiedName(element)}'.")
400 ex.add_note(f"Expected one of: {subtypeSymbol._possibleReferences}.")
401 raise ex
402 else:
403 ex = TypeError(f"Found element '{subtypeSymbol._name._identifier}', but it is not a type or subtype.")
404 ex.add_note(f"Got type '{getFullyQualifiedName(element)}'.")
405 raise ex
406 except KeyError:
407 key = subtypeSymbol._name._identifier
409 if (parentNamespace := self._parentNamespace) is None:
410 raise ExtendedKeyError(key, (self, ), f"Subtype '{key}' not found in '{self._name}'.")
412 try:
413 return parentNamespace.FindSubtype(subtypeSymbol)
414 except ExtendedKeyError as ex:
415 searchedNamespaces = (self, *ex.searchedNamespaces)
416 raise ExtendedKeyError(key, searchedNamespaces, f"Subtype '{key}' not found in: {', '.join(ns._name for ns in searchedNamespaces)}.") from ex
418 def FindObject(self, objectSymbol: Symbol) -> Obj:
419 try:
420 element = self._LookupElement(objectSymbol._name._normalizedIdentifier)
421 if isinstance(element, Signal):
422 if PossibleReference.Signal in objectSymbol._possibleReferences:
423 return element
424 elif PossibleReference.SignalAttribute in objectSymbol._possibleReferences:
425 return element
426 else:
427 ex = TypeError(f"Found signal '{objectSymbol._name._identifier}', but it was not expected.")
428 ex.add_note(f"Got type '{getFullyQualifiedName(element)}'.")
429 ex.add_note(f"Expected one of: {objectSymbol._possibleReferences}.")
430 raise ex
431 elif isinstance(element, Constant):
432 if PossibleReference.Constant in objectSymbol._possibleReferences: 432 ↛ 435line 432 didn't jump to line 435 because the condition on line 432 was always true
433 return element
434 else:
435 ex = TypeError(f"Found constant '{objectSymbol._name._identifier}', but it was not expected.")
436 ex.add_note(f"Got type '{getFullyQualifiedName(element)}'.")
437 ex.add_note(f"Expected one of: {objectSymbol._possibleReferences}.")
438 raise ex
439 elif isinstance(element, Variable):
440 if PossibleReference.Variable in objectSymbol._possibleReferences: 440 ↛ 443line 440 didn't jump to line 443 because the condition on line 440 was always true
441 return element
442 else:
443 ex = TypeError(f"Found variable '{objectSymbol._name._identifier}', but it was not expected.")
444 ex.add_note(f"Got type '{getFullyQualifiedName(element)}'.")
445 ex.add_note(f"Expected one of: {objectSymbol._possibleReferences}.")
446 raise ex
447 else:
448 ex = TypeError(f"Found element '{objectSymbol._name._identifier}', but it is not an object.")
449 ex.add_note(f"Got type '{getFullyQualifiedName(element)}'.")
450 raise ex
451 except KeyError:
452 key = objectSymbol._name._identifier
454 if (parentNamespace := self._parentNamespace) is None:
455 raise ExtendedKeyError(key, (self, ), f"Object '{key}' not found in '{self._name}'.")
457 try:
458 return parentNamespace.FindObject(objectSymbol)
459 except ExtendedKeyError as ex:
460 searchedNamespaces = (self, *ex.searchedNamespaces)
461 raise ExtendedKeyError(key, searchedNamespaces, f"Object '{key}' not found in: {', '.join(ns._name for ns in searchedNamespaces)}.") from ex