diff --git a/packtype/grammar/declarations.py b/packtype/grammar/declarations.py index 1ca863a..0464cd1 100644 --- a/packtype/grammar/declarations.py +++ b/packtype/grammar/declarations.py @@ -16,7 +16,7 @@ from ..types.base import Base from ..types.constant import Constant from ..types.enum import Enum, EnumMode -from ..types.normative import NormativePoint, Priority +from ..types.requirement import Priority, RequirementTag from ..types.scalar import Scalar from ..types.struct import Struct from ..types.union import Union @@ -407,8 +407,8 @@ def to_class( @dataclass -class DeclNormative: - """Represents a normative point declaration.""" +class DeclRequirement: + """Represents a requirement tag declaration.""" position: Position name: str @@ -423,9 +423,10 @@ def to_class( ], int | type[Base], ], - ) -> type[NormativePoint]: - entity = build_from_fields(NormativePoint, self.name, {}, {"priority": self.priority}) + ) -> type[RequirementTag]: + entity = build_from_fields(RequirementTag, self.name, {}, {"priority": self.priority}) entity.__doc__ = str(self.description) if self.description else None + entity.priority = self.priority return entity diff --git a/packtype/grammar/grammar.py b/packtype/grammar/grammar.py index f276e1e..f0e007a 100644 --- a/packtype/grammar/grammar.py +++ b/packtype/grammar/grammar.py @@ -21,8 +21,8 @@ DeclEnum, DeclImport, DeclInstance, - DeclNormative, DeclPackage, + DeclRequirement, DeclScalar, DeclStruct, DeclUnion, @@ -227,7 +227,7 @@ def _resolve(ref: str | ForeignRef) -> int: package._pt_attach(obj := decl.to_class(source, _resolve)) # Remember this type known_entities[decl.name] = (obj, decl.position) - case DeclNormative(): + case DeclRequirement(): # Check for name collisions _check_collision(decl.name) obj = decl.to_class(_resolve) diff --git a/packtype/grammar/packtype.lark b/packtype/grammar/packtype.lark index 30e47af..b460ae3 100644 --- a/packtype/grammar/packtype.lark +++ b/packtype/grammar/packtype.lark @@ -73,7 +73,7 @@ modifier: "@" name "=" (name | ESCAPED_STRING | NUMERIC) | decl_enum | decl_struct | decl_union - | decl_normative + | decl_requirement // ============================================================================= // Package @@ -213,16 +213,16 @@ decl_union: "union"i name "{" descr? modifier* field* "}" // ============================================================================= -// Normative Points +// Requirement Tags // -// vnorm my_feature : P1 "This feature is high priority" +// requirement my_feature : P1 "This feature is high priority" // // ============================================================================= // Priority keywords PRIORITY: /P[0-9]+/ -decl_normative: "vnorm"i name ":" PRIORITY descr? +decl_requirement: "requirement"i name ":" PRIORITY descr? // ============================================================================= // Expressions @@ -241,7 +241,10 @@ expr: expr_term (OPERATOR expr_term)* expr_funcs: name "(" (expr ","?)* ")" NUMERIC: HEX | BINARY | DECIMAL -HEX: (/\b0x[0-9a-f]+\b/i) + +// Allowing the following format 0x0000_0000_... +HEX: (/\b0x[0-9a-f]+(?:_[0-9a-f]+)*\b/i) + BINARY: (/\b0b[0-1]+\b/) DECIMAL: SIGN? /\b[0-9]+\b/ OPERATOR: SIGN diff --git a/packtype/grammar/transformer.py b/packtype/grammar/transformer.py index 25b1517..d25480b 100644 --- a/packtype/grammar/transformer.py +++ b/packtype/grammar/transformer.py @@ -12,7 +12,7 @@ from ..common.expression import Expression, ExpressionFunction from ..types.assembly import Packing from ..types.enum import EnumMode -from ..types.normative import Priority +from ..types.requirement import Priority from .declarations import ( DeclAlias, DeclConstant, @@ -21,8 +21,8 @@ DeclField, DeclImport, DeclInstance, - DeclNormative, DeclPackage, + DeclRequirement, DeclScalar, DeclStruct, DeclUnion, @@ -307,8 +307,8 @@ def decl_union(self, meta, body): return DeclUnion(Position(meta.line, meta.column), name, description, mods, remainder) @v_args(meta=True) - def decl_normative(self, meta, body): - """Transform a normative point declaration.""" + def decl_requirement(self, meta, body): + """Transform a requirement tag declaration.""" name, priority_token, *remainder = body if remainder and isinstance(remainder[0], Description): descr, *remainder = remainder @@ -317,7 +317,7 @@ def decl_normative(self, meta, body): priority = Priority[priority_token.value.upper()] - return DeclNormative(Position(meta.line, meta.column), name, priority, descr) + return DeclRequirement(Position(meta.line, meta.column), name, priority, descr) def variant_default(self, body): return VariantCondition(conditions=None) diff --git a/packtype/types/array.py b/packtype/types/array.py index a60d760..b24ce18 100644 --- a/packtype/types/array.py +++ b/packtype/types/array.py @@ -5,7 +5,12 @@ import functools import math from collections.abc import Callable, Iterable -from typing import Any, Self +from typing import Any + +try: + from typing import Self +except ImportError: + from typing_extensions import Self # noqa: UP035 from .bitvector import BitVector, BitVectorWindow from .numeric import Numeric @@ -190,6 +195,20 @@ def __str__(self) -> str: lines.append(f"- Entry[{i}]: {self._pt_entries[i]!s}") return "\n".join(lines) + def __copy__(self) -> "PackedArray": + """ + Copy this object by unpacking and then packing it again. + :return: A copy of this object + """ + return self._pt_spec._pt_unpack(self._pt_pack()) + + def __deepcopy__(self, _memo: dict) -> "PackedArray": + """ + Reuse __copy__, + which should already be a deep copy for objects that pack and unpack cleanly. + """ + return self.__copy__() + class UnpackedArray: def __init__( diff --git a/packtype/types/assembly.py b/packtype/types/assembly.py index 51d6dea..3d45e15 100644 --- a/packtype/types/assembly.py +++ b/packtype/types/assembly.py @@ -8,6 +8,11 @@ from textwrap import indent from typing import Any +try: + from typing import Self +except ImportError: + from typing_extensions import Self # noqa: UP035 + from ..svg.render import ElementStyle, SvgConfig, SvgField, SvgRender from .array import ArraySpec, PackedArray from .base import Base @@ -123,7 +128,8 @@ def __getattribute__(self, fname: str): return finst # Is this the padding field? elif fname == "_padding" and self._PT_PADDING > 0: - padding = Scalar[self._PT_PADDING](_pt_bv=self._pt_bv) + lsb, msb = self._PT_RANGES["_padding"] + padding = Scalar[self._PT_PADDING](_pt_bv=self._pt_bv.create_window(msb, lsb)) self._pt_force_set("_padding", padding) return padding # If not resolved, forward the attribute error @@ -382,7 +388,7 @@ def _pt_pack(self) -> int: return int(self._pt_bv) @classmethod - def _pt_unpack(cls, packed: int) -> "PackedAssembly": + def _pt_unpack(cls, packed: int) -> Self: inst = cls() inst._pt_set(packed) return inst diff --git a/packtype/types/base.py b/packtype/types/base.py index 0ed4d85..0c1056d 100644 --- a/packtype/types/base.py +++ b/packtype/types/base.py @@ -117,13 +117,3 @@ def _list_objs(): print(f"{cnt:10d}: {obj}") # noqa: T201 atexit.register(_list_objs) - - def __copy__(self) -> Self: - raise NotImplementedError( - "Please use packtype.utils.basic.copy() to copy a Packtype definition" - ) - - def __deepcopy__(self, _memo: dict[int, Any]) -> Self: - raise NotImplementedError( - "Please use packtype.utils.basic.copy() to copy a Packtype definition" - ) diff --git a/packtype/types/enum.py b/packtype/types/enum.py index a0252ec..4c64ca0 100644 --- a/packtype/types/enum.py +++ b/packtype/types/enum.py @@ -6,6 +6,11 @@ import math from typing import Any +try: + from typing import Self +except ImportError: + from typing_extensions import Self # noqa: UP035 + from .base import Base from .bitvector import BitVector, BitVectorWindow from .constant import Constant @@ -154,8 +159,12 @@ def _pt_as_dict(cls) -> dict[str, int]: return {n: int(v) for v, n in cls._PT_LKP_INST.items()} @classmethod - def _pt_cast(cls, value: int) -> None: + def _pt_cast(cls, value: int) -> Self: if value in cls._PT_LKP_VALUE: return cls._PT_LKP_VALUE[value] else: return cls(value) + + @classmethod + def _pt_unpack(cls, packed: int) -> Self: + return cls._pt_cast(packed) diff --git a/packtype/types/numeric.py b/packtype/types/numeric.py index 978e98d..e7a32a2 100644 --- a/packtype/types/numeric.py +++ b/packtype/types/numeric.py @@ -3,6 +3,12 @@ # +try: + from typing import Self +except ImportError: + from typing_extensions import Self # noqa: UP035 + + class Numeric: def __int__(self) -> int: raise NotImplementedError("Subclass must implement __int__") @@ -248,3 +254,26 @@ def __ge__(self, other) -> bool: def __hash__(self) -> int: return id(self) + + @classmethod + def _pt_unpack(cls, _packed: int) -> Self: + """ + Unpack the object from an integer + :param packed: The value to unpack + :return: The unpacked object + """ + raise NotImplementedError + + def __copy__(self) -> Self: + """ + Copy this object by unpacking and then packing it again + :return: A copy of this object + """ + return self._pt_unpack(int(self)) + + def __deepcopy__(self, _memo: dict) -> Self: + """ + Reuse __copy__, + which should already be a deep copy for objects that pack and unpack cleanly. + """ + return self.__copy__() diff --git a/packtype/types/package.py b/packtype/types/package.py index 19ad6e8..8adfb92 100644 --- a/packtype/types/package.py +++ b/packtype/types/package.py @@ -13,8 +13,8 @@ from .base import Base from .constant import Constant from .enum import Enum -from .normative import NormativePoint from .primitive import NumericType +from .requirement import RequirementTag from .scalar import ScalarType from .struct import Struct from .union import Union @@ -24,13 +24,13 @@ class Package(Base): _PT_ALLOW_DEFAULTS: list[type[Base]] = [Constant] _PT_FIELDS: dict - _PT_NORMS: dict + _PT_REQUIREMENTS: dict @classmethod def _pt_construct(cls, parent: Base) -> None: super()._pt_construct(parent) cls._PT_FIELDS = {} - cls._PT_NORMS = {} + cls._PT_REQUIREMENTS = {} for fname, ftype, fval in cls._pt_definitions(): if inspect.isclass(ftype) and issubclass(ftype, Constant): cls._pt_attach_constant(fname, ftype(default=fval)) @@ -45,9 +45,9 @@ def _pt_attach_constant(cls, fname: str, finst: Constant) -> Constant: return finst @classmethod - def _pt_attach_norm(cls, fname: str, finst: NormativePoint) -> NormativePoint: + def _pt_attach_norm(cls, fname: str, finst: RequirementTag) -> RequirementTag: finst._PT_ATTACHED_TO = cls - cls._PT_NORMS[fname] = finst + cls._PT_REQUIREMENTS[fname] = finst cls._PT_FIELDS[finst] = finst setattr(cls, fname, finst) return finst @@ -170,8 +170,11 @@ def _pt_unions(self) -> Iterable[tuple[str, Union]]: return self._pt_filter_for_class(Union) @property - def _pt_norms(self) -> Iterable[tuple[str, NormativePoint]]: - return ((vnorm_name, vnorm_inst) for vnorm_name, vnorm_inst in self._PT_NORMS.items()) + def _pt_reqs(self) -> Iterable[tuple[str, RequirementTag]]: + return ( + (requirement_name, requirement_inst) + for requirement_name, requirement_inst in self._PT_REQUIREMENTS.items() + ) @property def _pt_structs_and_unions(self) -> Iterable[tuple[str, Struct | Union]]: diff --git a/packtype/types/primitive.py b/packtype/types/primitive.py index a4aeca5..72cf289 100644 --- a/packtype/types/primitive.py +++ b/packtype/types/primitive.py @@ -81,10 +81,10 @@ def value(self) -> int: return int(self._pt_bv) @value.setter - def value(self, value: int) -> int: + def value(self, value: int) -> None: self._pt_set(value) - def _pt_set(self, value: int) -> int: + def _pt_set(self, value: int) -> None: value = int(value) if value < 0 or (self._pt_width > 0 and value > self._pt_mask): raise PrimitiveValueError( @@ -98,6 +98,10 @@ def __int__(self) -> int: def __float__(self) -> float: return float(int(self)) + @classmethod + def _pt_unpack(cls, packed: int) -> Self: + return cls(packed) + class NumericPrimitive(NumericType, metaclass=MetaPrimitive): @classmethod diff --git a/packtype/types/normative.py b/packtype/types/requirement.py similarity index 71% rename from packtype/types/normative.py rename to packtype/types/requirement.py index 0917f64..208827f 100644 --- a/packtype/types/normative.py +++ b/packtype/types/requirement.py @@ -5,7 +5,7 @@ class Priority(Enum): - """Priority levels for normative points.""" + """Priority levels for requirement tags.""" P0 = "P0" P1 = "P1" @@ -17,12 +17,12 @@ def __str__(self): return self.value -class NormativePoint(Base): +class RequirementTag(Base): """ - Represents a normative point declaration inside a Packtype package. + Represents a RequirementTag declaration inside a Packtype package. - A NormativePoint marks an element with a defined priority level - (P0-P3) and optional description. Created when parsing `vnorm` + A RequirementTag marks an element with a defined priority level + (P0-P3) and optional description. Created when parsing `requirement` declarations. """ @@ -39,8 +39,8 @@ def __init__(self, name: str, priority: Priority, description: str | None = None def __repr__(self) -> str: """Compact debug-friendly representation.""" if self.description: - return f"" - return f"" + return f"" + return f"" def __str__(self) -> str: """Human-readable string representation.""" diff --git a/packtype/utils/basic.py b/packtype/utils/basic.py index 3359a6a..e33e240 100644 --- a/packtype/utils/basic.py +++ b/packtype/utils/basic.py @@ -12,6 +12,7 @@ from ..types.assembly import PackedAssembly from ..types.base import Base from ..types.enum import Enum +from ..types.numeric import Numeric from ..types.primitive import NumericType from ..types.scalar import ScalarType from ..types.union import Union @@ -143,13 +144,11 @@ def unpack[T: Base](ptype: type[T], value: int) -> T: raise TypeError(f"{ptype} is an instance of a Packtype definition") if not issubclass(ptype, Base): raise TypeError(f"{ptype} is not a Packtype definition") - if issubclass(ptype, NumericType): - return ptype(value) - elif issubclass(ptype, Enum): - return ptype._pt_cast(value) - else: + if issubclass(ptype, Numeric): return ptype._pt_unpack(value) + raise TypeError(f"{ptype} (type {type(ptype)}) does not support unpacking") + def pack(pinst: Base) -> int: """ diff --git a/pyproject.toml b/pyproject.toml index 8f55577..570c172 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.masonry.api" [tool.poetry] name = "packtype" -version = "3.1.0" +version = "3.1.1" description = "Packed data structure specifications for multi-language hardware projects" authors = ["Peter Birch "] license = "Apache-2.0" diff --git a/tests/grammar/test_normative.py b/tests/grammar/test_requirement.py similarity index 68% rename from tests/grammar/test_normative.py rename to tests/grammar/test_requirement.py index 02798d2..1308525 100644 --- a/tests/grammar/test_normative.py +++ b/tests/grammar/test_requirement.py @@ -5,26 +5,26 @@ import pytest from packtype.grammar import ParseError, parse_string -from packtype.types.normative import Priority +from packtype.types.requirement import Priority from ..fixtures import reset_registry assert reset_registry -def test_parse_norm(): - """Test that normative point attributes are correctly set.""" +def test_parse_req(): + """Test that requirement tag attributes are correctly set.""" pkg = next( parse_string( """ package the_package { - vnorm my_feature : P1 "This feature is high priority" + requirement my_feature : P1 "This feature is high priority" } """ ) ) - assert "my_feature" in pkg._PT_NORMS - assert pkg._PT_NORMS["my_feature"]._PT_ATTRIBUTES["priority"] == Priority.P1 + assert "my_feature" in pkg._PT_REQUIREMENTS + assert pkg._PT_REQUIREMENTS["my_feature"]._PT_ATTRIBUTES["priority"] == Priority.P1 assert pkg.my_feature._PT_ATTRIBUTES["priority"] == Priority.P1 @@ -35,7 +35,7 @@ def test_parse_erroneous_priority(): parse_string( """ package the_package { - vnorm my_feature : P "This feature is high priority" + requirement my_feature : P "This feature is high priority" } """ ) @@ -49,7 +49,7 @@ def test_parse_priority_typo(): parse_string( """ package the_package { - vnorm my_feature : P5 "This feature's priority is wrong!" + requirement my_feature : P5 "This feature's priority is wrong!" } """ ) diff --git a/tests/pysyntax/test_struct.py b/tests/pysyntax/test_struct.py index d5a1b4b..6aeea48 100644 --- a/tests/pysyntax/test_struct.py +++ b/tests/pysyntax/test_struct.py @@ -430,3 +430,19 @@ class Level0: assert "Level0: 0x12345678" in output0 assert output0.count("├─") + output0.count("└─") == 8 # All fields visible assert "│ │" in output0 # Should have 3-level nesting indicators + + +def test_struct_padding(): + """Test that the padding field captures data correctly""" + + @packtype.package() + class TestPkg: + pass + + @TestPkg.struct(width=8) + class TestStruct: + a: Scalar[4] + + inst = TestStruct._pt_unpack(0xAB) + assert inst.a.value == 0xB + assert inst._padding.value == 0xA diff --git a/tests/utils/test_utils_basic.py b/tests/utils/test_utils_basic.py index ffa5a6d..14735c2 100644 --- a/tests/utils/test_utils_basic.py +++ b/tests/utils/test_utils_basic.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 # +import copy import math import pytest @@ -418,7 +419,7 @@ class TestEnum: C: Constant = 0x3 enum_a = TestEnum.A - enum_a_copy = utils.copy(enum_a) + enum_a_copy = copy.copy(enum_a) assert enum_a == enum_a_copy enum_a += 1 assert enum_a != enum_a_copy @@ -439,7 +440,7 @@ class TestByte: high: TestPkg.nibble struct_a = utils.unpack(TestByte, 0xFF) - struct_a_copy = utils.copy(struct_a) + struct_a_copy = copy.copy(struct_a) struct_a_equals = struct_a assert struct_a == struct_a_copy == struct_a_copy # Mutate original struct a @@ -464,7 +465,7 @@ class TestUnion: raw: Scalar[8] union_a = utils.unpack(TestUnion, 0xFF) - union_a_copy = utils.copy(union_a) + union_a_copy = copy.copy(union_a) union_a_equals = union_a assert union_a == union_a_copy == union_a_copy # Mutate original struct a @@ -492,11 +493,13 @@ class ArrayPkg: byte_2d_array = utils.unpack(ArrayPkg.Byte2DArray, 0x12345678) byte_array_equals = byte_array - byte_array_copy = utils.copy(byte_array) + byte_array_copy = copy.copy(byte_array) + assert byte_array is not byte_array_copy assert byte_array == byte_array_copy == byte_array_equals byte_2d_array_equals = byte_2d_array - byte_2d_array_copy = utils.copy(byte_2d_array) + byte_2d_array_copy = copy.copy(byte_2d_array) + assert byte_2d_array is not byte_2d_array_copy assert byte_2d_array == byte_2d_array_copy == byte_2d_array_equals # Mutate arrays @@ -508,3 +511,156 @@ class ArrayPkg: assert byte_2d_array == byte_2d_array_equals assert byte_2d_array_copy != byte_2d_array + + +def test_deepcopy_enum(): + """ + Test deep copying an enum instance + """ + + @packtype.package() + class TestPkg: + nibble: Scalar[4] + + @TestPkg.enum() + class TestEnum: + A: Constant = 0x1 + B: Constant = 0x2 + C: Constant = 0x3 + + enum_a = TestEnum.A + enum_a_deepcopy = copy.deepcopy(enum_a) + assert enum_a == enum_a_deepcopy + enum_a += 1 + assert enum_a != enum_a_deepcopy + + +def test_deepcopy_struct(): + """ + Test that deepcopy works for structs + """ + + @packtype.package() + class TestPkg: + nibble: Scalar[4] + + @TestPkg.struct() + class TestByte: + low: TestPkg.nibble + high: TestPkg.nibble + + struct_a = utils.unpack(TestByte, 0xFF) + struct_a_deepcopy = copy.deepcopy(struct_a) + struct_a_equals = struct_a + assert struct_a == struct_a_deepcopy == struct_a_equals + # Mutate original struct a + struct_a.low = 0x0 + assert struct_a_equals == struct_a + assert struct_a_deepcopy != struct_a + + +def test_deepcopy_union(): + @packtype.package() + class TestPkg: + nibble: Scalar[4] + + @TestPkg.struct() + class TestByte: + low: TestPkg.nibble + high: TestPkg.nibble + + @TestPkg.union() + class TestUnion: + struct: TestByte + raw: Scalar[8] + + union_a = utils.unpack(TestUnion, 0xFF) + union_a_deepcopy = copy.deepcopy(union_a) + union_a_equals = union_a + assert union_a == union_a_deepcopy == union_a_equals + # Mutate original struct a + union_a.struct.low = 0x0 + assert union_a_equals == union_a + assert union_a_deepcopy != union_a + + +def test_deepcopy_array(): + @packtype.package() + class TestPkg: + nibble: Scalar[4] + + @TestPkg.struct() + class TestByte: + low: TestPkg.nibble + high: TestPkg.nibble + + @packtype.package() + class ArrayPkg: + ByteArray: TestPkg.TestByte[4] + Byte2DArray: TestPkg.TestByte[2][2] + + byte_array = utils.unpack(ArrayPkg.ByteArray, 0x12345678) + byte_2d_array = utils.unpack(ArrayPkg.Byte2DArray, 0x12345678) + + byte_array_equals = byte_array + byte_array_deepcopy = copy.deepcopy(byte_array) + assert byte_array is not byte_array_deepcopy + assert byte_array == byte_array_deepcopy == byte_array_equals + + byte_2d_array_equals = byte_2d_array + byte_2d_array_deepcopy = copy.deepcopy(byte_2d_array) + assert byte_2d_array is not byte_2d_array_deepcopy + assert byte_2d_array == byte_2d_array_deepcopy == byte_2d_array_equals + + # Mutate arrays + byte_array[0] = 0x00 + byte_2d_array[1][0] = 0x00 + + assert byte_array == byte_array_equals + assert byte_array != byte_array_deepcopy + + assert byte_2d_array == byte_2d_array_equals + assert byte_2d_array_deepcopy != byte_2d_array + + +def test_deepcopy_unpacked_array(): + @packtype.package() + class TestPkg: + nibble: Scalar[4] + + @TestPkg.struct() + class TestByte: + low: TestPkg.nibble + high: TestPkg.nibble + + @packtype.package() + class ArrayPkg: + ByteArray: TestPkg.TestByte[4] + + byte_array_spec = ArrayPkg.ByteArray + unpacked_array = byte_array_spec.as_unpacked() + + # Set some initial values + unpacked_array[0] = utils.unpack(TestByte, 0x12) + unpacked_array[1] = utils.unpack(TestByte, 0x34) + unpacked_array[2] = utils.unpack(TestByte, 0x56) + unpacked_array[3] = utils.unpack(TestByte, 0x78) + + unpacked_array_equals = unpacked_array + unpacked_array_deepcopy = copy.deepcopy(unpacked_array) + assert unpacked_array is not unpacked_array_deepcopy + assert unpacked_array[0] == unpacked_array_deepcopy[0] + assert unpacked_array[1] == unpacked_array_deepcopy[1] + assert unpacked_array[2] == unpacked_array_deepcopy[2] + assert unpacked_array[3] == unpacked_array_deepcopy[3] + + # Mutate original array + unpacked_array[0] = utils.unpack(TestByte, 0x00) + unpacked_array[1] = utils.unpack(TestByte, 0x00) + + assert unpacked_array[0] == unpacked_array_equals[0] + assert unpacked_array[1] == unpacked_array_equals[1] + assert unpacked_array[0] != unpacked_array_deepcopy[0] + assert unpacked_array[1] != unpacked_array_deepcopy[1] + assert unpacked_array[2] == unpacked_array_deepcopy[2] + assert unpacked_array[3] == unpacked_array_deepcopy[3]