Skip to content

Commit f96604f

Browse files
committed
lib.data: make all layouts immutable.
This is actually an existing correctness requirement (for the similar reasons that ValueCastable.as_value() must always return the same value every time) that for some reason wasn't respected.
1 parent 52b9d3f commit f96604f

File tree

2 files changed

+37
-67
lines changed

2 files changed

+37
-67
lines changed

amaranth/lib/data.py

Lines changed: 32 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ class Layout(ShapeCastable, metaclass=ABCMeta):
8484
It is an abstract base class; :class:`StructLayout`, :class:`UnionLayout`,
8585
:class:`ArrayLayout`, and :class:`FlexibleLayout` implement concrete layout rules.
8686
New layout rules can be defined by inheriting from this class.
87+
88+
Like all other shape-castable objects, all layouts are immutable. New classes deriving from
89+
:class:`Layout` must preserve this invariant.
8790
"""
8891

8992
@staticmethod
@@ -274,14 +277,6 @@ class StructLayout(Layout):
274277
"""
275278

276279
def __init__(self, members):
277-
self.members = members
278-
279-
@property
280-
def members(self):
281-
return {key: field.shape for key, field in self._fields.items()}
282-
283-
@members.setter
284-
def members(self, members):
285280
offset = 0
286281
self._fields = {}
287282
if not isinstance(members, Mapping):
@@ -300,6 +295,10 @@ def members(self, members):
300295
self._fields[key] = Field(shape, offset)
301296
offset += cast_shape.width
302297

298+
@property
299+
def members(self):
300+
return {key: field.shape for key, field in self._fields.items()}
301+
303302
def __iter__(self):
304303
return iter(self._fields.items())
305304

@@ -348,14 +347,6 @@ class UnionLayout(Layout):
348347
Dictionary of union members.
349348
"""
350349
def __init__(self, members):
351-
self.members = members
352-
353-
@property
354-
def members(self):
355-
return {key: field.shape for key, field in self._fields.items()}
356-
357-
@members.setter
358-
def members(self, members):
359350
self._fields = {}
360351
if not isinstance(members, Mapping):
361352
raise TypeError("Union layout members must be provided as a mapping, not {!r}"
@@ -372,6 +363,10 @@ def members(self, members):
372363
.format(shape)) from e
373364
self._fields[key] = Field(shape, 0)
374365

366+
@property
367+
def members(self):
368+
return {key: field.shape for key, field in self._fields.items()}
369+
375370
def __iter__(self):
376371
return iter(self._fields.items())
377372

@@ -429,34 +424,26 @@ class ArrayLayout(Layout):
429424
Amount of elements.
430425
"""
431426
def __init__(self, elem_shape, length):
432-
self.elem_shape = elem_shape
433-
self.length = length
434-
435-
@property
436-
def elem_shape(self):
437-
return self._elem_shape
438-
439-
@elem_shape.setter
440-
def elem_shape(self, elem_shape):
441427
try:
442428
Shape.cast(elem_shape)
443429
except TypeError as e:
444430
raise TypeError("Array layout element shape must be a shape-castable object, "
445431
"not {!r}"
446432
.format(elem_shape)) from e
433+
if not isinstance(length, int) or length < 0:
434+
raise TypeError("Array layout length must be a non-negative integer, not {!r}"
435+
.format(length))
447436
self._elem_shape = elem_shape
437+
self._length = length
438+
439+
@property
440+
def elem_shape(self):
441+
return self._elem_shape
448442

449443
@property
450444
def length(self):
451445
return self._length
452446

453-
@length.setter
454-
def length(self, length):
455-
if not isinstance(length, int) or length < 0:
456-
raise TypeError("Array layout length must be a non-negative integer, not {!r}"
457-
.format(length))
458-
self._length = length
459-
460447
def __iter__(self):
461448
offset = 0
462449
for index in range(self._length):
@@ -519,39 +506,14 @@ class FlexibleLayout(Layout):
519506
Fields defined in the layout.
520507
"""
521508
def __init__(self, size, fields):
522-
self.size = size
523-
self.fields = fields
524-
525-
@property
526-
def size(self):
527-
""":meta private:""" # work around Sphinx bug
528-
return self._size
529-
530-
@size.setter
531-
def size(self, size):
532509
if not isinstance(size, int) or size < 0:
533510
raise TypeError("Flexible layout size must be a non-negative integer, not {!r}"
534511
.format(size))
535-
if hasattr(self, "_fields") and self._fields:
536-
endmost_name, endmost_field = max(self._fields.items(),
537-
key=lambda pair: pair[1].offset + pair[1].width)
538-
if endmost_field.offset + endmost_field.width > size:
539-
raise ValueError("Flexible layout size {} does not cover the field '{}', which "
540-
"ends at bit {}"
541-
.format(size, endmost_name,
542-
endmost_field.offset + endmost_field.width))
543-
self._size = size
544-
545-
@property
546-
def fields(self):
547-
return {**self._fields}
548-
549-
@fields.setter
550-
def fields(self, fields):
551-
self._fields = {}
552512
if not isinstance(fields, Mapping):
553513
raise TypeError("Flexible layout fields must be provided as a mapping, not {!r}"
554514
.format(fields))
515+
self._size = size
516+
self._fields = {}
555517
for key, field in fields.items():
556518
if not isinstance(key, (int, str)) or (isinstance(key, int) and key < 0):
557519
raise TypeError("Flexible layout field name must be a non-negative integer or "
@@ -560,12 +522,21 @@ def fields(self, fields):
560522
if not isinstance(field, Field):
561523
raise TypeError("Flexible layout field value must be a Field instance, not {!r}"
562524
.format(field))
563-
if field.offset + field.width > self._size:
525+
if field.offset + field.width > size:
564526
raise ValueError("Flexible layout field '{}' ends at bit {}, exceeding "
565527
"the size of {} bit(s)"
566-
.format(key, field.offset + field.width, self._size))
528+
.format(key, field.offset + field.width, size))
567529
self._fields[key] = field
568530

531+
@property
532+
def size(self):
533+
""":meta private:""" # work around Sphinx bug
534+
return self._size
535+
536+
@property
537+
def fields(self):
538+
return {**self._fields}
539+
569540
def __iter__(self):
570541
return iter(self._fields.items())
571542

tests/test_lib_data.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,11 @@ def test_construct(self):
263263
self.assertEqual(il["b"], Field(unsigned(3), 0))
264264
self.assertEqual(il[0], Field(unsigned(2), 5))
265265

266+
def test_is_not_mutated(self):
267+
il = FlexibleLayout(8, {"a": Field(unsigned(1), 0)})
268+
del il.fields["a"]
269+
self.assertIn("a", il.fields)
270+
266271
def test_eq(self):
267272
self.assertEqual(FlexibleLayout(3, {"a": Field(unsigned(1), 0)}),
268273
FlexibleLayout(3, {"a": Field(unsigned(1), 0)}))
@@ -325,12 +330,6 @@ def test_size_wrong_small(self):
325330
r"^Flexible layout field 'a' ends at bit 5, exceeding the size of 4 bit\(s\)$"):
326331
FlexibleLayout(4, {"a": Field(unsigned(2), 3)})
327332

328-
def test_size_wrong_shrink(self):
329-
il = FlexibleLayout(8, {"a": Field(unsigned(2), 3)})
330-
with self.assertRaisesRegex(ValueError,
331-
r"^Flexible layout size 4 does not cover the field 'a', which ends at bit 5$"):
332-
il.size = 4
333-
334333
def test_key_wrong_missing(self):
335334
il = FlexibleLayout(8, {"a": Field(unsigned(2), 3)})
336335
with self.assertRaisesRegex(KeyError,

0 commit comments

Comments
 (0)