11import bisect
22
33from amaranth .lib import wiring
4- from amaranth .utils import bits_for
54
65
76__all__ = ["ResourceInfo" , "MemoryMap" ]
@@ -48,6 +47,60 @@ def items(self):
4847 yield key , self ._values [key ]
4948
5049
50+ class _Namespace :
51+ """Namespace.
52+
53+ A set of path-like names that identify locations in a hierarchical structure.
54+ """
55+ def __init__ (self ):
56+ self ._assignments = dict ()
57+
58+ def is_available (self , * names , reasons = None ):
59+ for name in names :
60+ assert name and isinstance (name , tuple )
61+ assert all (part and isinstance (part , str ) for part in name )
62+
63+ conflicts = False
64+ for name_idx , name in enumerate (names ):
65+ # Also check for conflicts between the queried names.
66+ reserved_names = sorted (self ._assignments .keys () | set (names [name_idx + 1 :]))
67+
68+ for reserved_name in reserved_names :
69+ # A conflict happens in the following cases:
70+ # - `name` is equal to `reserved_name`
71+ # - `name` is a prefix of `reserved_name`
72+ # - `reserved_name` is a prefix of `name`.
73+ for part_idx , part in enumerate (name ):
74+ if part != reserved_name [part_idx ]:
75+ # Available. `name` and `reserved_name` are not equal and are not prefixes
76+ # of each other.
77+ break
78+ if part_idx == min (len (name ), len (reserved_name )) - 1 :
79+ # `name` and `reserved_name` are equal so far, but we have reached the end
80+ # of at least one of them. In either case, they are in conflict.
81+ conflicts = True
82+ # Conflicts between the queried names should never happen.
83+ assert reserved_name in self ._assignments
84+ if reasons is not None :
85+ reasons .append (f"{ name } conflicts with local name { reserved_name } "
86+ f"assigned to { self ._assignments [reserved_name ]!r} " )
87+ break
88+
89+ return not conflicts
90+
91+ def assign (self , name , obj ):
92+ assert self .is_available (name )
93+ self ._assignments [name ] = obj
94+
95+ def extend (self , other ):
96+ assert isinstance (other , _Namespace )
97+ assert self .is_available (* other .names ())
98+ self ._assignments .update (other ._assignments )
99+
100+ def names (self ):
101+ yield from self ._assignments .keys ()
102+
103+
51104class ResourceInfo :
52105 """Resource metadata.
53106
@@ -57,7 +110,7 @@ class ResourceInfo:
57110 ----------
58111 resource : :class:`wiring.Component`
59112 A resource located in the memory map. See :meth:`MemoryMap.add_resource` for details.
60- path : iter( str)
113+ path : :class:`tuple` of (:class:` str` or (:class:`tuple` of :class:`str`) )
61114 Path of the resource. It is composed of the names of each window sitting between
62115 the resource and the memory map from which this :class:`ResourceInfo` was obtained.
63116 See :meth:`MemoryMap.add_window` for details.
@@ -71,9 +124,12 @@ class ResourceInfo:
71124 is located behind a window that uses sparse addressing.
72125 """
73126 def __init__ (self , resource , path , start , end , width ):
74- if not path or not all (isinstance (part , str ) and part for part in path ):
75- raise TypeError ("Path must be a non-empty sequence of non-empty strings, not {!r}"
76- .format (path ))
127+ flattened_path = []
128+ for name in path :
129+ flattened_path += name if name and isinstance (name , tuple ) else [name ]
130+ if not (path and isinstance (path , tuple ) and
131+ all (name and isinstance (name , str ) for name in flattened_path )):
132+ raise TypeError (f"Path must be a non-empty tuple of non-empty strings, not { path !r} " )
77133 if not isinstance (start , int ) or start < 0 :
78134 raise TypeError ("Start address must be a non-negative integer, not {!r}"
79135 .format (start ))
@@ -162,7 +218,7 @@ def __init__(self, *, addr_width, data_width, alignment=0, name=None):
162218 self ._ranges = _RangeMap ()
163219 self ._resources = dict ()
164220 self ._windows = dict ()
165- self ._namespace = dict ()
221+ self ._namespace = _Namespace ()
166222
167223 self ._next_addr = 0
168224 self ._frozen = False
@@ -266,8 +322,8 @@ def add_resource(self, resource, *, name, size, addr=None, alignment=None):
266322 ---------
267323 resource : :class:`wiring.Component`
268324 The resource to be added.
269- name : str
270- Name of the resource. It must not collide with the name of other resources or windows
325+ name : :class:`tuple` of (:class:` str`)
326+ Name of the resource. It must not conflict with the name of other resources or windows
271327 present in this memory map.
272328 addr : int
273329 Address of the resource. Optional. If ``None``, the implicit next address will be used.
@@ -295,7 +351,8 @@ def add_resource(self, resource, *, name, size, addr=None, alignment=None):
295351 :exc:`ValueError`
296352 If the resource has already been added to this memory map.
297353 :exc:`ValueError`
298- If the name of the resource is already present in the namespace of this memory map.
354+ If the resource name conflicts with the name of other resources or windows present in
355+ this memory map.
299356 """
300357 if self ._frozen :
301358 raise ValueError ("Memory map has been frozen. Cannot add resource {!r}"
@@ -309,10 +366,17 @@ def add_resource(self, resource, *, name, size, addr=None, alignment=None):
309366 raise ValueError ("Resource {!r} is already added at address range {:#x}..{:#x}"
310367 .format (resource , addr_range .start , addr_range .stop ))
311368
312- if not isinstance (name , str ) or not name :
313- raise TypeError ("Name must be a non-empty string, not {!r}" .format (name ))
314- if name in self ._namespace :
315- raise ValueError ("Name {} is already used by {!r}" .format (name , self ._namespace [name ]))
369+ if not (name and isinstance (name , tuple ) and
370+ all (part and isinstance (part , str ) for part in name )):
371+ raise TypeError (f"Resource name must be a non-empty tuple of non-empty strings, not "
372+ f"{ name !r} " )
373+
374+ reasons = []
375+ if not self ._namespace .is_available (name , reasons = reasons ):
376+ reasons_as_string = "" .join (f"\n - { reason } " for reason in reasons )
377+ raise ValueError (f"Resource { resource !r} cannot be added to the local namespace:" +
378+ reasons_as_string )
379+ del reasons
316380
317381 if alignment is not None :
318382 if not isinstance (alignment , int ) or alignment < 0 :
@@ -325,7 +389,7 @@ def add_resource(self, resource, *, name, size, addr=None, alignment=None):
325389 addr_range = self ._compute_addr_range (addr , size , alignment = alignment )
326390 self ._ranges .insert (addr_range , resource )
327391 self ._resources [id (resource )] = resource , name , addr_range
328- self ._namespace [ name ] = resource
392+ self ._namespace . assign ( name , resource )
329393 self ._next_addr = addr_range .stop
330394 return addr_range .start , addr_range .stop
331395
@@ -382,17 +446,24 @@ def add_window(self, window, *, addr=None, sparse=None):
382446
383447 Exceptions
384448 ----------
385- Raises :exn:`ValueError` if one of the following occurs:
386-
387- - this memory map is frozen;
388- - the requested address and size, after alignment, would overlap with any resources or
389- windows that have already been added, or would be out of bounds;
390- - the added memory map has a wider datapath than this memory map;
391- - dense address translation is used and the datapath width of this memory map is not an
392- integer multiple of the datapath of the added memory map;
393- - the name of the added memory map is already present in the namespace of this memory map;
394- - the added memory map has no name, and the name of one of its subordinate resources or
395- windows is already present in the namespace of this memory map;
449+ :exc:`ValueError`
450+ If the memory map is frozen.
451+ :exc:`TypeError`
452+ If the added memory map is not a :class:`MemoryMap`.
453+ :exc:`ValueError`
454+ If the requested address and size, after alignment, would overlap with any resources or
455+ windows that have already been added, or would be out of bounds.
456+ :exc:`ValueError`
457+ If the added memory map has a wider datapath than this memory map.
458+ :exc:`ValueError`
459+ If dense address translation is used and the datapath width of this memory map is not
460+ an integer multiple of the datapath of the added memory map.
461+ :exc:`ValueError`
462+ If the name of the added memory map conflicts with the name of other resources or
463+ windows present in this memory map.
464+ :exc:`ValueError`
465+ If the added memory map has no name, and the name of one of its resources or windows
466+ conflicts with the name of others present in this memory map.
396467 """
397468 if not isinstance (window , MemoryMap ):
398469 raise TypeError ("Window must be a MemoryMap, not {!r}"
@@ -423,17 +494,13 @@ def add_window(self, window, *, addr=None, sparse=None):
423494 "data width {}"
424495 .format (self .data_width , window .data_width ))
425496
426- if window .name is None :
427- name_conflicts = sorted (self ._namespace .keys () & window ._namespace .keys ())
428- if name_conflicts :
429- name_conflict_descrs = ["{} is used by {!r}" .format (name , self ._namespace [name ])
430- for name in name_conflicts ]
431- raise ValueError ("The following names are already used: {}"
432- .format ("; " .join (name_conflict_descrs )))
433- else :
434- if window .name in self ._namespace :
435- raise ValueError ("Name {} is already used by {!r}"
436- .format (window .name , self ._namespace [window .name ]))
497+ queries = window ._namespace .names () if window .name is None else ((window .name ,),)
498+ reasons = []
499+ if not self ._namespace .is_available (* queries , reasons = reasons ):
500+ reasons_as_string = "" .join (f"\n - { reason } " for reason in reasons )
501+ raise ValueError (f"Window { window !r} cannot be added to the local namespace:" +
502+ reasons_as_string )
503+ del queries , reasons
437504
438505 if not sparse :
439506 ratio = self .data_width // window .data_width
@@ -452,9 +519,9 @@ def add_window(self, window, *, addr=None, sparse=None):
452519 self ._ranges .insert (addr_range , window )
453520 self ._windows [id (window )] = window , addr_range
454521 if window .name is None :
455- self ._namespace .update (window ._namespace )
522+ self ._namespace .extend (window ._namespace )
456523 else :
457- self ._namespace [ window .name ] = window
524+ self ._namespace . assign (( window .name ,), window )
458525 self ._next_addr = addr_range .stop
459526 return addr_range .start , addr_range .stop , addr_range .step
460527
@@ -587,3 +654,6 @@ def decode_address(self, address):
587654 return assignment .decode_address ((address - addr_range .start ) // addr_range .step )
588655 else :
589656 assert False # :nocov:
657+
658+ def __repr__ (self ):
659+ return f"MemoryMap(name={ self .name !r} )"
0 commit comments