diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 7cdfaf1..ea5b733 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -25,10 +25,10 @@ jobs: timeout-minutes: 15 steps : - uses: actions/checkout@v4 - - name: Set up Python 3.11 + - name: Set up Python 3.13 uses: actions/setup-python@v5 with: - python-version: 3.11 + python-version: 3.13 - name : Install Poetry and environment shell: bash run : | diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 849bd2f..ca893fe 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -13,10 +13,10 @@ jobs: timeout-minutes: 15 steps : - uses: actions/checkout@v4 - - name: Set up Python 3.11 + - name: Set up Python 3.13 uses: actions/setup-python@v5 with: - python-version: 3.11 + python-version: 3.13 - name : Install Poetry and environment shell: bash run : | diff --git a/.github/workflows/pr_tests.yml b/.github/workflows/pr_tests.yml index 6eb8ef2..53ad869 100644 --- a/.github/workflows/pr_tests.yml +++ b/.github/workflows/pr_tests.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.11", "3.12", "3.13"] + python-version: ["3.12", "3.13"] steps: - uses: actions/checkout@v2 diff --git a/docs/syntax/constant.md b/docs/syntax/constant.md index 56668f1..09f877a 100644 --- a/docs/syntax/constant.md +++ b/docs/syntax/constant.md @@ -14,7 +14,7 @@ custom grammar: from packtype import Constant @packtype.package() - class MyPackage: + class ThePackage: VALUE_A : Constant = 123 VALUE_B : Constant[16] = 234 ``` @@ -22,7 +22,7 @@ custom grammar: === "Packtype (.pt)" ```sv linenums="1" - package my_package { + package the_package { VALUE_A : constant = 123 "Comments may be attached to values with a string following the definition" VALUE_B : constant[16] = 234 @@ -38,12 +38,12 @@ custom grammar: As rendered to SystemVerilog: ```sv linenums="1" -package my_package; +package the_package; localparam VALUE_A = 123; localparam bit [15:0] VALUE_B = 234; -endpackage : my_package +endpackage : the_package ``` ## Syntax @@ -58,7 +58,7 @@ allocate it. ```python linenums="1" @packtype.package() - class MyPackage: + class ThePackage: # Format: : Constant = MY_CONSTANT : Constant = 123 ``` @@ -66,7 +66,7 @@ allocate it. === "Packtype (.pt)" ```sv linenums="1" - package my_package { + package the_package { // Format: : constant = MY_CONSTANT : constant = 123 } @@ -79,7 +79,7 @@ from the declaration: ```python linenums="1" @packtype.package() -class MyPackage: +class ThePackage: # Format: = MY_CONSTANT = 123 ``` @@ -99,7 +99,7 @@ number of bits. ```python linenums="1" @packtype.package() - class MyPackage: + class ThePackage: # Format: : Constant[] = MY_CONSTANT : Constant[8] = 123 ``` @@ -107,7 +107,7 @@ number of bits. === "Packtype (.pt)" ```sv linenums="1" - package my_package { + package the_package { // Format: : constant[] = MY_CONSTANT : constant[8] = 123 } @@ -122,7 +122,7 @@ from other constant definitions within the package. ```python linenums="1" @packtype.package() - class MyPackage: + class ThePackage: DOUBLE_WIDTH : Constant = 32 VALUE_A : Constant = 9 VALUE_B : Constant = 3 @@ -132,7 +132,7 @@ from other constant definitions within the package. === "Packtype (.pt)" ```sv linenums="1" - package my_package { + package the_package { DOUBLE_WIDTH : constant = 32 VALUE_A : constant = 9 VALUE_B : constant = 3 @@ -154,7 +154,7 @@ that is outside the legal range a `ValueError` will be raised: ```python linenums="1" @packtype.package() - class MyPackage: + class ThePackage: # Attempt to store 123 in a 4 bit value MY_CONSTANT : Constant[4] = 123 ``` @@ -162,7 +162,7 @@ that is outside the legal range a `ValueError` will be raised: === "Packtype (.pt)" ```sv linenums="1" - package my_package { + package the_package { // Attempt to store 123 in a 4 bit value MY_CONSTANT : constant[4] = 123 } diff --git a/docs/syntax/docstrings.md b/docs/syntax/docstrings.md index bf95710..f4ec391 100644 --- a/docs/syntax/docstrings.md +++ b/docs/syntax/docstrings.md @@ -13,14 +13,14 @@ Descriptions can be added with normal [Python docstrings]((https://peps.python.o from packtype import Constant @packtype.package() - class MyPackage: + class ThePackage: """ Package decription, using normal Python docstrings """ ... - @MyPackage.enum() + @ThePackage.enum() class Fruit: """ Class description, @@ -35,7 +35,7 @@ Descriptions can be added with normal [Python docstrings]((https://peps.python.o === "Packtype (.pt)" ```sv linenums="1" - package my_package { + package the_package { """ Package description using multiline docstring syntax diff --git a/docs/syntax/enum.md b/docs/syntax/enum.md index 97f65f7..33086e0 100644 --- a/docs/syntax/enum.md +++ b/docs/syntax/enum.md @@ -15,10 +15,10 @@ custom grammar: from packtype import Constant @packtype.package() - class MyPackage: + class ThePackage: ... - @MyPackage.enum() + @ThePackage.enum() class Fruit: """Description of the enumeration can go here""" APPLE : Constant @@ -30,7 +30,7 @@ custom grammar: === "Packtype (.pt)" ```sv linenums="1" - package my_package { + package the_package { enum fruit_t { "Description of the enumeration can go here" @prefix=FRUIT @@ -45,7 +45,7 @@ custom grammar: As rendered to SystemVerilog: ```sv linenums="1" -package my_package; +package the_package; typedef enum logic [1:0] { FRUIT_APPLE, @@ -54,7 +54,7 @@ typedef enum logic [1:0] { FRUIT_BANANA } fruit_t; -endpackage : my_package +endpackage : the_package ``` @@ -151,7 +151,7 @@ values and continue to enumerate from that point: import packtype from packtype import Constant - @MyPackage.enum(width=8) + @ThePackage.enum(width=8) class Opcodes: ALU_ADD : Constant = 0x10 ALU_SUB : Constant # Infers 0x11 @@ -167,7 +167,7 @@ values and continue to enumerate from that point: === "Packtype (.pt)" ```sv linenums="1" - package my_package { + package the_package { enum [8] opcodes_t { ALU_ADD : constant = 0x10 ALU_SUB : constant // Infers 0x11 diff --git a/docs/syntax/instance.md b/docs/syntax/instance.md index 367db5d..c90ac41 100644 --- a/docs/syntax/instance.md +++ b/docs/syntax/instance.md @@ -13,31 +13,31 @@ custom grammar: from packtype import Constant, Scalar @packtype.package() - class MyPackage: + class ThePackage: pass - @MyPackage.enum() + @ThePackage.enum() class Month: JAN : Constant FEB : Constant ... DEC : Constant - @MyPackage.struct() + @ThePackage.struct() class Date: year : Scalar[12] month : Month day : Scalar[5] - MyPackage._pt_attach_instance("SUMMER_START", Month.JUN) - MyPackage._pt_attach_instance("SUMMER_END", Month.AUG) - MyPackage._pt_attach_instance("CHRISTMAS", Date(year=2025, month=Month.DEC, day=25)) + ThePackage._pt_attach_instance("SUMMER_START", Month.JUN) + ThePackage._pt_attach_instance("SUMMER_END", Month.AUG) + ThePackage._pt_attach_instance("CHRISTMAS", Date(year=2025, month=Month.DEC, day=25)) ``` === "Packtype (.pt)" ```sv linenums="1" - package my_package { + package the_package { enum month_e { JAN : constant FEB : constant @@ -64,7 +64,7 @@ custom grammar: As rendered to SystemVerilog: ```sv linenums="1" -package my_package; +package the_package; localparam month_e SUMMER_START = month_e'(7); @@ -77,5 +77,5 @@ localparam date_t CHRISTMAS = '{ , year: 12'h7E9 }; -endpackage : my_package +endpackage : the_package ``` diff --git a/docs/syntax/package.md b/docs/syntax/package.md index dfe7fd5..79e2332 100644 --- a/docs/syntax/package.md +++ b/docs/syntax/package.md @@ -13,7 +13,7 @@ custom grammar: import packtype @packtype.package() - class MyPackage: + class ThePackage: """Description of what purpose this package serves""" ... ``` @@ -21,7 +21,7 @@ custom grammar: === "Packtype (.pt)" ```sv linenums="1" - package my_package { + package the_package { """Description of what purpose this package serves""" // ... } @@ -30,11 +30,11 @@ custom grammar: As rendered to SystemVerilog: ```sv linenums="1" -package my_package; +package the_package; // ...attached definitions... -endpackage : my_package +endpackage : the_package ``` ## Imports diff --git a/docs/syntax/scalar.md b/docs/syntax/scalar.md index 75bd393..8c918e6 100644 --- a/docs/syntax/scalar.md +++ b/docs/syntax/scalar.md @@ -14,7 +14,7 @@ custom grammar: from packtype import Constant, Scalar @packtype.package() - class MyPackage: + class ThePackage: # Constants TYPE_A_W : Constant = 29 TYPE_B_W : Constant = 13 @@ -28,7 +28,7 @@ custom grammar: === "Packtype (.pt)" ```sv linenums="1" - package my_package { + package the_package { // Constants TYPE_A_W : constant = 29 TYPE_B_W : constant = 13 @@ -49,7 +49,7 @@ custom grammar: As rendered to SystemVerilog: ```sv linenums="1" -package my_package; +package the_package; localparam TYPE_A_W = 29; localparam TYPE_B_W = 13; @@ -58,7 +58,7 @@ typedef logic [28:0] type_a_t; typedef logic [12:0] type_b_t; typedef logic [6:0] type_c_t; -endpackage : my_package +endpackage : the_package ``` !!! note @@ -78,7 +78,7 @@ to unsigned. ```python @packtype.package() -class MyPackage: +class ThePackage: # Format: : Scalar[, ] MyType : Scalar[123, False] ``` diff --git a/docs/syntax/struct.md b/docs/syntax/struct.md index 5c3369c..2d273b9 100644 --- a/docs/syntax/struct.md +++ b/docs/syntax/struct.md @@ -15,30 +15,30 @@ custom grammar: from packtype import Constant, Scalar @packtype.package() - class MyPackage: + class ThePackage: INSTR_W : Constant = 32 RegSel : Scalar[5] - @MyPackage.enum(width=8) + @ThePackage.enum(width=8) class Opcode: ADD : Constant = 0 SUB : Constant = 1 ... - @MyPackage.struct(width=MyPackage.INSTR_W) + @ThePackage.struct(width=ThePackage.INSTR_W) class Instruction: """Encodes an instruction to the CPU""" opcode : Opcode - rd : MyPackage.RegSel - rs1 : MyPackage.RegSel - rs2 : MyPackage.RegSel + rd : ThePackage.RegSel + rs1 : ThePackage.RegSel + rs2 : ThePackage.RegSel imm : Scalar[9] ``` === "Packtype (.pt)" ```sv linenums="1" - package my_package { + package the_package { INSTR_W : constant = 32 reg_sel_t : scalar[5] @@ -63,7 +63,7 @@ custom grammar: As rendered to SystemVerilog ```sv linenums="1" -package my_package; +package the_package; // ...supporting declarations... @@ -75,7 +75,7 @@ typedef struct packed { opcode_t opcode; } instruction_t; -endpackage : my_package +endpackage : the_package ``` !!! warning diff --git a/docs/syntax/union.md b/docs/syntax/union.md index d37cb84..83d36c9 100644 --- a/docs/syntax/union.md +++ b/docs/syntax/union.md @@ -15,10 +15,10 @@ custom grammar: from packtype import Constant, Scalar @packtype.package() - class MyPackage: + class ThePackage: PACKET_W : Constant = 33 - @MyPackage.struct(width=MyPackage.PACKET_W) + @ThePackage.struct(width=ThePackage.PACKET_W) class Header: command : Scalar[8] source : Scalar[8] @@ -26,14 +26,14 @@ custom grammar: length : Scalar[8] parity : Scalar[1] - @MyPackage.struct(width=MyPackage.PACKET_W) + @ThePackage.struct(width=ThePackage.PACKET_W) class Body: data : Scalar[32] parity : Scalar[1] - @MyPackage.union() + @ThePackage.union() class Packet: - raw : Scalar[MyPackage.PACKET_W] + raw : Scalar[ThePackage.PACKET_W] header : Header body : Body ``` @@ -41,7 +41,7 @@ custom grammar: === "Packtype (.pt)" ```sv linenums="1" - package my_package { + package the_package { PACKET_W : Constant = 33 struct [PACKET_W] header_t { @@ -68,7 +68,7 @@ custom grammar: As rendered to SystemVerilog ```sv linenums="1" -package my_package; +package the_package; // ...supporting declarations... @@ -78,7 +78,7 @@ typedef union packed { body_t body; } packet_t; -endpackage : my_package +endpackage : the_package ``` !!! note diff --git a/docs/syntax/variants.md b/docs/syntax/variants.md new file mode 100644 index 0000000..4925d95 --- /dev/null +++ b/docs/syntax/variants.md @@ -0,0 +1,118 @@ +Variants are a feature unique to the Packtype custom grammar (`.pt` files), and +allow definitions to be varied based on conditions presented to the parser. This +mechanism can be useful to allow different parts of a large project to operate +at different cadences while referring to a central set of definitions - by +allowing changes to be 'gated' on specific conditions, different consumers can +take updates at their own pace. + +## Example + +The example below shows how variants can be used to encapsulate two different +sizings for a bus, while common definitions can exist in just a single location: + +```sv linenums="1" + +package the_package { + variants { + "Encapsulate different options for the bus width" + + default { + "Default condition when no variant is selected" + DATA_BITS : constant = 64 + "Bit-width of the data bus" + MAX_STRIDES : constant = 4 + "Maximum number of strides of a packet" + } + + NARROW_BUS { + "Narrow bus variant - fewer data bits but more strides" + DATA_BITS : constant = 8 + "Bit width of the data bus" + MAX_STRIDES : constant = 32 + "Maximum number of strides of a packet" + } + + } + + DATA_BYTES : constant = DATA_BITS / 8 + "Bus width in bytes" + STRIDE_INDEX_WIDTH : constant = clog2(MAX_STRIDES) + "Index width to count through the strides" +} +``` + +If this was rendered to SystemVerilog without providing any variant conditions, +then the output would look like: + +```sv linenums="1" + +package the_package; + +localparam DATA_BITS = 64; +localparam MAX_STRIDES = 4; +localparam DATA_BYTES = 8; +localparam STRIDE_INDEX_WIDTH = 2; + +endpackage : the_package +``` + +While if you were to provide `NARROW_BUS` as a condition: + +```sv linenums="1" + +package the_package; + +localparam DATA_BITS = 8; +localparam MAX_STRIDES = 32; +localparam DATA_BYTES = 1; +localparam STRIDE_INDEX_WIDTH = 5; + +endpackage : the_package +``` + +!!! note + + While this example only shows constants being declared, any type, constant, + or enum declaration can be made within a `variants` block just as if it was + a native part of the package. + +## Default Variant + +A variants block must specify a `default` that is taken whenever no other condition +matches. If a variants block does not specify a `default`, then a `TransformerError` +will be raised during the parsing process. + +Only a single `default` may be specified per variants block, specifying more than +one `default` will cause a `VariantError` to be raised. + +## Variant Conditions + +Variant entries can use `and` / `or` keywords to create more complex statements, +these follow the same operator precedence as Python (`and` evaluated first, +followed by `or`). + +Where multiple conditions match, only the first variant block is considered. + +For example: + +```sv linenums="1" +package the_package { + variants { + COND_A and COND_B { + X : constant = 1 + } + COND_A { + Y : constant = 2 + } + default { + Z : constant = 3 + } + } +} +``` + +This means that where: + + * `COND_A` and `COND_B` are provided then ONLY `X = 1` will be evaluated; + * `COND_A` is provided then ONLY `Y = 2` will be evaluated; + * In any other scenario ONLY `Z = 3` will be evaluated. diff --git a/examples/variants/spec.pt b/examples/variants/spec.pt new file mode 100644 index 0000000..16c644f --- /dev/null +++ b/examples/variants/spec.pt @@ -0,0 +1,64 @@ +// Copyright 2023-2025, Peter Birch, mailto:peter@intuity.io +// SPDX-License-Identifier: Apache-2.0 +// + +package bus_defs { + "Definitions for a bus that demonstrate variants" + + variants { + default { + "Default bus configuration" + DATA_WIDTH : constant = 64 + "Bus data width in bits" + MAX_STRIDES : constant = 16 + "Maximum number of transaction strides" + } + NARROW { + "Used for narrow bus configuration" + DATA_WIDTH : constant = 16 + "Bus data width in bits" + MAX_STRIDES : constant = 64 + "Maximum number of transaction strides" + } + } + + TARGET_ID_W : constant = 4 + "Width of the target ID field" + SOURCE_ID_W : constant = 4 + "Width of the source ID field" + STRIDE_LEN_W : constant = clog2(MAX_STRIDES) + "Width of the stride length field" + + struct [DATA_WIDTH] header_t { + "Packet header" + source : scalar[SOURCE_ID_W] + "Source identifier" + target : scalar[TARGET_ID_W] + "Target identifier" + strides : scalar[STRIDE_LEN_W] + "Number of strides in the transaction" + } + + variants { + default { + "Default bus configuration does not offer a checksum" + union packet_u { + "Union of types carried on the bus" + header : header_t + body : scalar[DATA_WIDTH] + } + } + HAS_CHECKSUM { + "Bus configured to carry a checksum" + checksum_t : scalar[DATA_WIDTH] + "Checksum field" + union packet_u { + "Union of types carried on the bus (with checksum)" + header : header_t + body : scalar[DATA_WIDTH] + checksum : checksum_t + } + } + } + +} diff --git a/examples/variants/test.sh b/examples/variants/test.sh new file mode 100755 index 0000000..7224478 --- /dev/null +++ b/examples/variants/test.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +# Copyright 2023-2025, Peter Birch, mailto:peter@intuity.io +# SPDX-License-Identifier: Apache-2.0 +# + +# Credit to Dave Dopson: https://stackoverflow.com/questions/59895/how-can-i-get-the-source-directory-of-a-bash-script-from-within-the-script-itsel +this_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" + +# Setup PYTHONPATH to get access to packtype +export PYTHONPATH=${this_dir}/../..:$PYTHONPATH + +# Invoke packtype on Packtype syntax +python3 -m packtype --debug code package sv --type-filter none \ + --constant-filter none \ + ${this_dir}/out_pt \ + ${this_dir}/spec.pt \ + --variant WIDE \ + --variant HAS_CHECKSUM diff --git a/mkdocs.yml b/mkdocs.yml index 419b26f..9bfd7f0 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -43,6 +43,7 @@ nav: - Scalars: syntax/scalar.md - Structs: syntax/struct.md - Unions: syntax/union.md + - Variants: syntax/variants.md - Docstrings: syntax/docstrings.md - Utilities: - Basic: utilities/basic.md diff --git a/packtype/grammar/declarations.py b/packtype/grammar/declarations.py index d34124a..1ca863a 100644 --- a/packtype/grammar/declarations.py +++ b/packtype/grammar/declarations.py @@ -4,7 +4,9 @@ from collections.abc import Callable from dataclasses import dataclass +from enum import StrEnum from pathlib import Path +from typing import Any from .. import utils from ..common.expression import Expression @@ -14,6 +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.scalar import Scalar from ..types.struct import Struct from ..types.union import Union @@ -50,6 +53,9 @@ class Position: line: int column: int + def to_line_pointer(self, file: Path | None) -> str: + return f"{file.as_posix() if file else 'N/A'}:{self.line}" + @dataclass() class ForeignRef: @@ -400,6 +406,29 @@ def to_class( ) +@dataclass +class DeclNormative: + """Represents a normative point declaration.""" + + position: Position + name: str + priority: Priority + description: Description | None + + def to_class( + self, + cb_resolve: Callable[ + [ + str, + ], + int | type[Base], + ], + ) -> type[NormativePoint]: + entity = build_from_fields(NormativePoint, self.name, {}, {"priority": self.priority}) + entity.__doc__ = str(self.description) if self.description else None + return entity + + @dataclass() class DeclPackage: position: Position @@ -410,3 +439,90 @@ class DeclPackage: def get_modifiers(self) -> dict[str, str]: return {mod.option: mod.value for mod in (self.modifiers or {})} + + +class VariantOperator(StrEnum): + OR = "or" + AND = "and" + + +@dataclass() +class VariantCondition: + conditions: tuple["str | VariantOperator | VariantCondition"] | None + + @property + def is_default(self) -> bool: + return self.conditions is None + + def evaluate(self, conditions: list[str]) -> bool: + """ + Evaluate the condition terms and operators, prioritising AND over OR as + per Python's operator precedence. + + :param conditions: List of active condition strings + :return: Evaluation result + """ + if self.conditions is None: + return True + # Flatten nested VariantConditions and evaluate terms + flattened: list[bool | VariantOperator] = [] + for term in self.conditions: + match term: + case VariantCondition(): + flattened.append(term.evaluate(conditions)) + case VariantOperator(): + flattened.append(term) + case str(): + flattened.append(term in conditions) + # Collapse terms + while len(flattened) > 1: + lhs, op, rhs, *remainder = flattened + replacement = [] + match op: + case VariantOperator.AND: + replacement.append(lhs and rhs) + case VariantOperator.OR: + replacement.append(lhs or rhs) + flattened = replacement + remainder + # Return final result + assert isinstance(flattened[0], bool) + return flattened[0] + + +@dataclass() +class DeclVariant: + position: Position + condition: VariantCondition + description: Description | None + declarations: list + + def matches(self, conditions: list[str]) -> bool: + return self.condition.evaluate(conditions) + + +class VariantError(Exception): + pass + + +@dataclass() +class DeclVariantSet: + position: Position + description: Description | None + variants: list[DeclVariant] + + def to_declarations(self, conditions: list[str]) -> list[Any]: + default = None + for variant in self.variants: + # Pickup the default as we go past + if variant.condition.is_default: + if default is not None: + raise VariantError("Multiple default variants defined") + default = variant + # Check if the variant matches the requested conditions + elif variant.matches(conditions): + return variant.declarations + # If we get here, return the default if it exists + if default is not None: + return default.declarations + # Otherwise, raise an exception that a scenario can't be matched + raise VariantError("No matching variant can be determined") diff --git a/packtype/grammar/examples/package_a.pt b/packtype/grammar/examples/package_a.pt deleted file mode 100644 index 7dd838b..0000000 --- a/packtype/grammar/examples/package_a.pt +++ /dev/null @@ -1,7 +0,0 @@ -package package_a { - other_type_t : scalar[4] - - struct test_t { - field_a : other_type_t - } -} diff --git a/packtype/grammar/examples/package_b.pt b/packtype/grammar/examples/package_b.pt deleted file mode 100644 index 2cffcbd..0000000 --- a/packtype/grammar/examples/package_b.pt +++ /dev/null @@ -1,61 +0,0 @@ -// My package -package package_b { - "Describes the purpose of this package" - - import package_a::other_type_t - - local_type_t : other_type_t - - SIZED_CONSTANT : constant[8] = 4 - "With a description" - UNSIZED_CONSTANT : constant = 0x12345678 - CALCULATED : constant = (SIZED_CONSTANT + UNSIZED_CONSTANT) / 4 - - simple_type_t : scalar[SIZED_CONSTANT] - signed_type_t : signed scalar[SIZED_CONSTANT] - single_bit_t : scalar - "Description of the scalar" - - SOME_ENUM_W : constant = 2 - - enum gray [SOME_ENUM_W] gray_enum_e { - "This enum is implicitly assigned with Gray code values" - @prefix=GRAY - VALUE_A - VALUE_B - VALUE_C : constant - VALUE_D : constant - } - - enum indexed_enum_e { - "This enum is explicitly assigned with values" - @prefix=INDEXED - VALUE_A : constant = 0 - VALUE_B : constant = 1 - VALUE_C : constant = 2 - VALUE_D : constant = 3 - } - - SOME_STRUCT_W : constant = 8 + 1 + SOME_ENUM_W + 1 - - struct unsized_t { - "Implicitly sized structure" - field_a : simple_type_t - field_b : signed scalar[2] - field_c : single_bit_t - } - - struct msb [SOME_STRUCT_W] sized_t { - field_a : simple_type_t - field_b : scalar[1] - field_c : single_bit_t - field_d : gray_enum_e - field_e : other_type_t - } - - union my_union_t { - raw : scalar[SOME_STRUCT_W] - my_struct : sized_t - } - -} diff --git a/packtype/grammar/grammar.py b/packtype/grammar/grammar.py index 0773b41..f276e1e 100644 --- a/packtype/grammar/grammar.py +++ b/packtype/grammar/grammar.py @@ -4,7 +4,7 @@ import functools import inspect -from collections.abc import Iterable +from collections.abc import Iterator from pathlib import Path from lark import Lark @@ -21,10 +21,12 @@ DeclEnum, DeclImport, DeclInstance, + DeclNormative, DeclPackage, DeclScalar, DeclStruct, DeclUnion, + DeclVariantSet, ForeignRef, Position, ) @@ -64,9 +66,10 @@ def parse_string( definition: str, namespaces: dict[str, Package] | None = None, constant_overrides: dict[str, int] | None = None, + variants: list[str] | None = None, source: Path | None = None, keep_expression: bool = False, -) -> Iterable[Package]: +) -> Iterator[Package]: """ Parse a Packtype definition from a string producing a Package object. @@ -75,6 +78,7 @@ def parse_string( :param constant_overrides: Optional overrides for constants defined within the package, where the key must precisely match the constant's name + :param variants: An optional list of variant conditions to apply. :param source: An optional source path for error reporting and associating each declaration with its source file. :param keep_expression: If True, expressions will be attached to constants @@ -87,7 +91,7 @@ def parse_string( constant_overrides = constant_overrides or {} # Parse the definition try: - definitions = PacktypeTransformer().transform(create_parser().parse(definition)) + definitions = PacktypeTransformer(source).transform(create_parser().parse(definition)) except UnexpectedToken as exc: raise ParseError( f"Failed to parse {source.name if source else 'input'} on line {exc.line}: " @@ -134,8 +138,17 @@ def _resolve(ref: str | ForeignRef) -> int: source=(source.as_posix() if source else "N/A", defn.position.line), ) - # Run through the declarations + # First flatten variants to primary definitions if any exist + declarations = [] for decl in defn.declarations: + match decl: + case DeclVariantSet(): + declarations.extend(decl.to_declarations(variants or [])) + case _: + declarations.append(decl) + + # Run through the flattened declarations + for decl in declarations: match decl: # Imports case DeclImport(): @@ -214,6 +227,12 @@ 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(): + # Check for name collisions + _check_collision(decl.name) + obj = decl.to_class(_resolve) + package._pt_attach_norm(decl.name, obj) + known_entities[decl.name] = (obj, decl.position) case _: raise Exception(f"Unhandled declaration: {decl}") @@ -240,14 +259,16 @@ def _resolve(ref: str | ForeignRef) -> int: def parse( path: Path, namespaces: dict[str, Package] | None = None, + variants: list[str] | None = None, constant_overrides: dict[str, int] | None = None, keep_expression: bool = False, -) -> Iterable[Package]: +) -> Iterator[Package]: """ Parse a Packtype definition from a file path producing a Package object. :param path: The path to the Packtype definition file. :param namespaces: A dictionary of known packages to resolve imports. + :param variants: An optional list of variant conditions to apply. :param constant_overrides: Optional overrides for constants defined within the package, where the key must precisely match the constant's name. @@ -259,6 +280,7 @@ def parse( yield from parse_string( definition=fh.read(), namespaces=namespaces, + variants=variants, constant_overrides=constant_overrides, source=path, keep_expression=keep_expression, diff --git a/packtype/grammar/packtype.lark b/packtype/grammar/packtype.lark index ac1731d..30e47af 100644 --- a/packtype/grammar/packtype.lark +++ b/packtype/grammar/packtype.lark @@ -61,6 +61,20 @@ modifier: "@" name "=" (name | ESCAPED_STRING | NUMERIC) ?root: decl_package* // | decl_regblock +// ============================================================================= +// Common list of declaration types +// NOTE: This excludes variants as we don't support nested variants +// ============================================================================= + +?common_body: decl_import + | decl_alias + | decl_constant + | decl_scalar + | decl_enum + | decl_struct + | decl_union + | decl_normative + // ============================================================================= // Package // ============================================================================= @@ -68,13 +82,41 @@ modifier: "@" name "=" (name | ESCAPED_STRING | NUMERIC) // Define package outer decl_package: "package"i name "{" descr? modifier* package_body* "}" -?package_body: decl_import - | decl_alias - | decl_constant - | decl_scalar - | decl_enum - | decl_struct - | decl_union +?package_body: decl_variant_set + | common_body + +// ============================================================================= +// Variants +// +// variants { +// "Description of why variants are necessary" +// default { +// "Without overrides, use this type" +// my_type_t : scalar[4] +// } +// FEATURE_A and FEATURE_B { +// "When FEATURE_A and FEATURE_B are enabled, use this type" +// my_type_t : scalar[8] +// } +// } +// +// ============================================================================= + +variant_operator_or: "or"i +variant_operator_and: "and"i + +variant_default: "default"i + +// NOTE: AND takes precendence over OR (matches Python's behaviour) +variant_condition_and: name + | variant_condition_and variant_operator_and name + +variant_condition_or: variant_condition_and + | variant_condition_or variant_operator_or variant_condition_and + +decl_variant: (variant_default | variant_condition_or) "{" descr? common_body* "}" + +decl_variant_set: "variants"i "{" descr? decl_variant* "}" // ============================================================================= // Imports @@ -169,6 +211,19 @@ packing_mode_lsb: "lsb"i decl_union: "union"i name "{" descr? modifier* field* "}" + +// ============================================================================= +// Normative Points +// +// vnorm my_feature : P1 "This feature is high priority" +// +// ============================================================================= + +// Priority keywords +PRIORITY: /P[0-9]+/ + +decl_normative: "vnorm"i name ":" PRIORITY descr? + // ============================================================================= // Expressions // ============================================================================= diff --git a/packtype/grammar/transformer.py b/packtype/grammar/transformer.py index 0895986..25b1517 100644 --- a/packtype/grammar/transformer.py +++ b/packtype/grammar/transformer.py @@ -4,6 +4,7 @@ import math import textwrap +from pathlib import Path from lark import Transformer, v_args @@ -11,6 +12,7 @@ from ..common.expression import Expression, ExpressionFunction from ..types.assembly import Packing from ..types.enum import EnumMode +from ..types.normative import Priority from .declarations import ( DeclAlias, DeclConstant, @@ -19,10 +21,13 @@ DeclField, DeclImport, DeclInstance, + DeclNormative, DeclPackage, DeclScalar, DeclStruct, DeclUnion, + DeclVariant, + DeclVariantSet, Description, FieldAssignment, FieldAssignments, @@ -31,10 +36,20 @@ Position, Signed, Unsigned, + VariantCondition, + VariantOperator, ) +class TransformerError(Exception): + pass + + class PacktypeTransformer(Transformer): + def __init__(self, file_path: Path | None = None) -> None: + super().__init__() + self.file_path = file_path + def DECIMAL(self, body): # noqa: N802 return int(body, 10) @@ -291,6 +306,60 @@ def decl_union(self, meta, body): mods.append(remainder.pop(0)) 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.""" + name, priority_token, *remainder = body + if remainder and isinstance(remainder[0], Description): + descr, *remainder = remainder + else: + descr = None + + priority = Priority[priority_token.value.upper()] + + return DeclNormative(Position(meta.line, meta.column), name, priority, descr) + + def variant_default(self, body): + return VariantCondition(conditions=None) + + def variant_operator_or(self, body): + return VariantOperator.OR + + def variant_operator_and(self, body): + return VariantOperator.AND + + def variant_condition_and(self, body): + return VariantCondition(conditions=tuple(body)) + + def variant_condition_or(self, body): + return VariantCondition(conditions=tuple(body)) + + def variant_condition(self, body): + return VariantCondition(conditions=tuple(body)) + + @v_args(meta=True) + def decl_variant(self, meta, body): + return DeclVariant( + Position(meta.line, meta.column), + condition=body.pop(0), + description=body.pop(0) if body and isinstance(body[0], Description) else None, + declarations=body, + ) + + @v_args(meta=True) + def decl_variant_set(self, meta, body): + position = Position(meta.line, meta.column) + # Check for a default case + if not any((isinstance(x, DeclVariant) and x.condition.is_default) for x in body): + raise TransformerError( + f"Variant at {position.to_line_pointer(self.file_path)} is missing a default case." + ) + return DeclVariantSet( + position=position, + description=body.pop(0) if body and isinstance(body[0], Description) else None, + variants=body, + ) + @v_args(meta=True) def decl_package(self, meta, body): p_name, *remainder = body diff --git a/packtype/start.py b/packtype/start.py index ab4f7ba..88d394f 100644 --- a/packtype/start.py +++ b/packtype/start.py @@ -31,7 +31,7 @@ def resolve_to_object( - baseline: list[Package], + baseline: list[Package | File], *path: str, acceptable: tuple[type[Base]] | None = None, ) -> Base: @@ -60,7 +60,9 @@ def resolve_to_object( return resolved -def load_specification(spec_files: list[str], keep_expression: bool) -> list[Base]: +def load_specification( + spec_files: list[str], variant: list[str] | None, keep_expression: bool +) -> list[Package | File]: log = get_log() # If multiple specifications are provided, check they all use .pt format @@ -76,7 +78,7 @@ def load_specification(spec_files: list[str], keep_expression: bool) -> list[Bas get_log().debug(f"Loading specification: {item}") # Packtype grammar files if item.lower().endswith((".pt", ".packtype", ".ptype")): - for package in parse(Path(item), namespaces, keep_expression=keep_expression): + for package in parse(Path(item), namespaces, variant, keep_expression=keep_expression): namespaces[package.__name__] = package # If it ends with `.py` assume it's Python elif item.endswith(".py"): @@ -108,18 +110,19 @@ def main(debug: bool): @main.command() -@click.argument("spec_files", type=str, nargs=-1) +@click.option("--variant", type=str, multiple=True, default=[], help="Variant conditions to apply") @click.option("--keep-expression", is_flag=True, help="Attach parsed expressions to constants") -def inspect(spec_files: list[str], keep_expression: bool): +@click.argument("spec_files", type=str, nargs=-1) +def inspect(keep_expression: bool, variant: list[str], spec_files: list[str]): log = get_log() - baseline = load_specification(spec_files, keep_expression) + baseline = load_specification(spec_files, variant, keep_expression) log.warning("Use the 'baseline' namespace to inspect Packtype definitions") breakpoint() # noqa: T100 del baseline @main.command() -@click.argument("selection", type=str) +@click.option("--variant", type=str, multiple=True, default=[], help="Variant conditions to apply") @click.option( "-o", "--output", @@ -128,11 +131,12 @@ def inspect(spec_files: list[str], keep_expression: bool): required=False, help="Output file to write the SVG to. If not provided, prints to stdout.", ) +@click.argument("selection", type=str) @click.argument("spec_files", type=str, nargs=-1) -def svg(selection: str, output: Path | None, spec_files: list[str]): +def svg(variant: list[str], output: Path | None, selection: str, spec_files: list[str]): # Resolve selection to a struct resolved = resolve_to_object( - load_specification(spec_files, keep_expression=False), + load_specification(spec_files, variant, keep_expression=False), *selection.split("."), acceptable=(Struct,), ) @@ -199,6 +203,7 @@ def svg(selection: str, output: Path | None, spec_files: list[str]): help="Select filters to apply to type names", ) @click.option("--keep-expression", is_flag=True, help="Attach parsed expressions to constants") +@click.option("--variant", type=str, multiple=True, default=[], help="Variant conditions to apply") @click.argument("mode", type=click.Choice(("package", "register"), case_sensitive=False)) @click.argument( "language", @@ -216,17 +221,18 @@ def code( package_filter: list[str], constant_filter: list[str], type_filter: list[str], + keep_expression: bool, + variant: list[str], mode: str, language: str, outdir: Path, spec_files: list[str], - keep_expression: bool, ): """Render Packtype package definitions using a language template""" log = get_log() # Load the baseline - resolved = load_specification(spec_files, keep_expression) + resolved = load_specification(spec_files, variant, keep_expression) # Deferred imports for optional libraries from mako import exceptions diff --git a/packtype/types/array.py b/packtype/types/array.py index 6b476d2..a60d760 100644 --- a/packtype/types/array.py +++ b/packtype/types/array.py @@ -8,13 +8,15 @@ from typing import Any, Self from .bitvector import BitVector, BitVectorWindow +from .numeric import Numeric from .packing import Packing class ArraySpec: - def __init__(self, base: Any, dimensions: int | tuple[int]) -> None: + def __init__(self, base: Any, dimensions: int | tuple[int], attached_to: Any = None) -> None: self.base = base self.dimensions = dimensions if isinstance(dimensions, list | tuple) else (dimensions,) + self._PT_ATTACHED_TO = attached_to @property def _pt_flat_dimension(self) -> int: @@ -81,7 +83,7 @@ def __getitem__(self, key: int) -> Self: return type(self)(self.base, (key, *self.dimensions)) -class PackedArray: +class PackedArray(Numeric): def __init__( self, spec: ArraySpec, @@ -182,6 +184,12 @@ def __int__(self) -> int: def _pt_set(self, value: int) -> None: self._pt_bv.set(value) + def __str__(self) -> str: + lines = [f"{type(self).__name__} - {len(self)} entries: 0x{int(self):X}"] + for i in range(len(self)): + lines.append(f"- Entry[{i}]: {self._pt_entries[i]!s}") + return "\n".join(lines) + class UnpackedArray: def __init__( diff --git a/packtype/types/assembly.py b/packtype/types/assembly.py index 3d81d64..51d6dea 100644 --- a/packtype/types/assembly.py +++ b/packtype/types/assembly.py @@ -5,6 +5,7 @@ import functools import math from collections.abc import Iterable +from textwrap import indent from typing import Any from ..svg.render import ElementStyle, SvgConfig, SvgField, SvgRender @@ -129,19 +130,104 @@ def __getattribute__(self, fname: str): else: raise e - def __str__(self) -> str: - lines = [f"{type(self).__name__}: 0x{int(self):X}"] - max_bits = math.ceil(math.log(self._PT_WIDTH, 10)) - max_name = max(map(len, self._PT_DEF.keys())) - for fname in self._PT_DEF.keys(): - finst = getattr(self, fname) - lsb, msb = self._PT_RANGES[fname] - width = msb - lsb + 1 + def _gather_str_tree_fields( + self, max_depth: int = 0, *, offset: int = 0, depth: int = 0, prefix: str = "" + ) -> list[tuple[int, int, str, int, str, bool]]: + """Recursively gather all fields with their hierarchy information. + + Used internally by _str_tree(). + + Returns list of (msb, lsb, name, value, tree_prefix, is_last) tuples. + """ + fields = [] + # Get fields sorted by MSB in descending order (higher bits first) + fields_msb_desc = self._pt_fields_msb_desc + + for idx, (lsb, msb, (fname, finst)) in enumerate(fields_msb_desc): + abs_msb, abs_lsb = msb + offset, lsb + offset + is_last = idx == len(fields_msb_desc) - 1 + + fields.append((abs_msb, abs_lsb, fname, int(finst), prefix, is_last)) + + if isinstance(finst, PackedAssembly): + if (max_depth <= 0) or (depth + 1 < max_depth): + # Extend the tree prefix: add │ if more siblings to connect to, else add spaces + child_prefix = prefix + (" " if is_last else "│ ") + fields.extend( + finst._gather_str_tree_fields( + max_depth, offset=abs_lsb, depth=depth + 1, prefix=child_prefix + ) + ) + + return fields + + def _format_str_tree_fields( + self, + fields: list[tuple[int, int, str, int, str, bool]], + ) -> list[str]: + """Format fields as aligned tree-style lines. + + Used internally by _str_tree(). + + Args: + fields: List of (msb, lsb, name, value, tree_prefix, is_last) tuples + """ + if not fields: + return [] + + # How many decimal digits to represent the field bit ranges with + max_bit_idx_digits = math.ceil(math.log(self._PT_WIDTH, 10)) + + # First pass: build line parts as tuples + line_parts = [] + for msb, lsb, name, value, prefix, is_last in fields: + branch = "└─" if is_last else "├─" + bit_range = f"[{msb:{max_bit_idx_digits}}:{lsb:>{max_bit_idx_digits}}]" + branch_name = f"{prefix}{branch} {name}" + # Calculate number of hex digits needed for the field width + width = (msb - lsb) + 1 + hex_width = (width + 3) // 4 + hex_value = f"0x{value:0{hex_width}X}" + line_parts.append((bit_range, branch_name, hex_value)) + + # Second pass: find max widths of columns to align them + max_bit_width = max(len(bits) for bits, _, _ in line_parts) + max_name_width = max(len(name) for _, name, _ in line_parts) + + # Third pass: format with aligned columns + lines = [] + for bit_range, branch_name, hex_value in line_parts: lines.append( - f" |- [{msb:{max_bits}}:{lsb:{max_bits}}] {fname:{max_name}} " - f"= 0x{int(finst):0{(width + 3) // 4}X}" + f"{bit_range:<{max_bit_width}} {branch_name:<{max_name_width}} = {hex_value}" ) - return "\n".join(lines) + + return lines + + def _str_tree(self, max_depth: int = 0) -> str: + """Return tree-style string representation with optional depth limit. + + Args: + max_depth: Maximum nesting depth (0=unlimited, 1=top-level only, etc.) + + Example: + BUS_DATA_T: 0x123456789ABCDEF0 + [1110:1097] ├─ checksum = 0x0000 + [1096: 57] └─ payload = 0x... + [1096:1081] │ ├─ header = 0x... + [1080: 57] │ └─ length = 0x0000 + ... + """ + header_str = f"{type(self).__name__}: 0x{int(self):X}" + + fields = self._gather_str_tree_fields(max_depth) + if not fields: + return header_str + + body = indent("\n".join(self._format_str_tree_fields(fields)), " ") + return f"{header_str}\n{body}" + + def __str__(self) -> str: + return self._str_tree(max_depth=1) def __repr__(self) -> str: return self.__str__() diff --git a/packtype/types/base.py b/packtype/types/base.py index 0c1056d..0ed4d85 100644 --- a/packtype/types/base.py +++ b/packtype/types/base.py @@ -117,3 +117,13 @@ 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/bitvector.py b/packtype/types/bitvector.py index d71983b..31340fe 100644 --- a/packtype/types/bitvector.py +++ b/packtype/types/bitvector.py @@ -85,18 +85,17 @@ def set(self, value: int, msb: int | None = None, lsb: int | None = None) -> Non # Otherwise mask and insert the value else: # If width is not set, attempt to infer it - width = self.__width - if width is None: - width = ceil(log2(max(value, self.__value))) + if self.__width is None: + self.__width = ceil(log2(max(value, self.__value))) # Default LSB/MSB lsb = lsb if lsb is not None else 0 - msb = msb if msb is not None else (width - 1) - # Sanity chedck arguments - assert msb < width, f"MSB of {msb} exceeds width {width}" + msb = msb if msb is not None else (self.__width - 1) + # Sanity check arguments + assert msb < self.__width, f"MSB of {msb} exceeds width {self.__width}" assert lsb >= 0, f"LSB of {lsb} is not supported" # Update value with masking mask = ((1 << (msb - lsb + 1)) - 1) << lsb - inv_mask = ((1 << width) - 1) ^ mask + inv_mask = ((1 << self.__width) - 1) ^ mask self.__value = (self.__value & inv_mask) | ((value << lsb) & mask) @@ -114,6 +113,7 @@ def __init__(self, bitvector: BitVector, msb: int, lsb: int) -> None: self.__bitvector = bitvector self.__msb = msb self.__lsb = lsb + self.__width = (msb - lsb) + 1 @property def msb(self) -> int: @@ -128,7 +128,7 @@ def lsb(self) -> int: @property def width(self) -> int: """Return the window's width""" - return (self.__msb - self.__lsb) + 1 + return self.__width def __int__(self) -> int: """Cast the window to an int by extracting the right bit range""" @@ -155,8 +155,11 @@ def set(self, value: int, msb: int | None = None, lsb: int | None = None) -> Non :param msb: MSB of the sub-window, defaults to width - 1 :param lsb: LSB of the sub-window, defaults to 0 """ - msb = (self.width - 1) if msb is None else msb - lsb = 0 if lsb is None else lsb - assert lsb >= 0, f"LSB of {lsb} is not supported" - assert msb < self.width, f"MSB of {msb} is not supported" - return self.__bitvector.set(value, msb + self.__lsb, lsb + self.__lsb) + if msb is None and lsb is None: + return self.__bitvector.set(value, self.__msb, self.__lsb) + else: + msb = (self.__width - 1) if msb is None else msb + lsb = 0 if lsb is None else lsb + assert lsb >= 0, f"LSB of {lsb} is not supported" + assert msb < self.__width, f"MSB of {msb} is not supported" + return self.__bitvector.set(value, msb + self.__lsb, lsb + self.__lsb) diff --git a/packtype/types/normative.py b/packtype/types/normative.py new file mode 100644 index 0000000..0917f64 --- /dev/null +++ b/packtype/types/normative.py @@ -0,0 +1,52 @@ +from enum import Enum +from typing import Any + +from .base import Base + + +class Priority(Enum): + """Priority levels for normative points.""" + + P0 = "P0" + P1 = "P1" + P2 = "P2" + P3 = "P3" + P4 = "P4" + + def __str__(self): + return self.value + + +class NormativePoint(Base): + """ + Represents a normative point declaration inside a Packtype package. + + A NormativePoint marks an element with a defined priority level + (P0-P3) and optional description. Created when parsing `vnorm` + declarations. + """ + + _PT_ATTRIBUTES: dict[str, tuple[Any, list[Any]]] = { + "priority": (Priority.P0, list(Priority)), + } + + def __init__(self, name: str, priority: Priority, description: str | None = None): + super().__init__() + self.name = name + self.priority = priority + self.description = description + + def __repr__(self) -> str: + """Compact debug-friendly representation.""" + if self.description: + return f"" + return f"" + + def __str__(self) -> str: + """Human-readable string representation.""" + return f"{self.name} ({self.priority.value})" + + @property + def __doc__(self) -> str | None: + """Expose the description as a docstring-like attribute.""" + return self.description diff --git a/packtype/types/package.py b/packtype/types/package.py index 27c5f28..19ad6e8 100644 --- a/packtype/types/package.py +++ b/packtype/types/package.py @@ -3,7 +3,7 @@ # import inspect -from collections.abc import Iterable +from collections.abc import Callable, Iterable from typing import Any from ordered_set import OrderedSet as OSet @@ -13,6 +13,7 @@ from .base import Base from .constant import Constant from .enum import Enum +from .normative import NormativePoint from .primitive import NumericType from .scalar import ScalarType from .struct import Struct @@ -23,11 +24,13 @@ class Package(Base): _PT_ALLOW_DEFAULTS: list[type[Base]] = [Constant] _PT_FIELDS: dict + _PT_NORMS: dict @classmethod def _pt_construct(cls, parent: Base) -> None: super()._pt_construct(parent) cls._PT_FIELDS = {} + cls._PT_NORMS = {} for fname, ftype, fval in cls._pt_definitions(): if inspect.isclass(ftype) and issubclass(ftype, Constant): cls._pt_attach_constant(fname, ftype(default=fval)) @@ -41,6 +44,14 @@ def _pt_attach_constant(cls, fname: str, finst: Constant) -> Constant: cls._PT_FIELDS[finst] = fname return finst + @classmethod + def _pt_attach_norm(cls, fname: str, finst: NormativePoint) -> NormativePoint: + finst._PT_ATTACHED_TO = cls + cls._PT_NORMS[fname] = finst + cls._PT_FIELDS[finst] = finst + setattr(cls, fname, finst) + return finst + @classmethod def _pt_attach_instance(cls, fname: str, finst: Base) -> Base: setattr(cls, fname, finst) @@ -113,20 +124,34 @@ def _pt_instances(self) -> Iterable[tuple[str, Base]]: if isinstance(x, Base) and not isinstance(x, Constant) ) - def _pt_filter_for_class(self, ctype: type[Base]) -> Iterable[tuple[str, type[Base]]]: - return ( - (y, x) - for x, y in self._pt_fields.items() - if (inspect.isclass(x) and issubclass(x, ctype)) - ) + def _pt_filter_for[T: Base | ArraySpec | ScalarType]( + self, type_filter: Callable[[T], bool] + ) -> Iterable[tuple[str, T]]: + return ((y, x) for x, y in self._pt_fields.items() if type_filter(x)) + + def _pt_filter_for_class[T: Base | ScalarType | Alias]( + self, ctype: type[T] + ) -> Iterable[tuple[str, T]]: + return self._pt_filter_for(lambda x: inspect.isclass(x) and issubclass(x, ctype)) @property def _pt_scalars(self) -> Iterable[tuple[str, ScalarType]]: return self._pt_filter_for_class(ScalarType) + @property + def _pt_all_types( + self, + ) -> Iterable[tuple[str, type[ScalarType | Enum | Struct | Union | ArraySpec]]]: + return self._pt_filter_for( + lambda x: ( + isinstance(x, ArraySpec) + or (inspect.isclass(x) and issubclass(x, ScalarType | Enum | Struct | Union)) + ) + ) + @property def _pt_arrays(self) -> Iterable[tuple[str, ArraySpec]]: - return ((y, x) for x, y in self._pt_fields.items() if isinstance(x, ArraySpec)) + return self._pt_filter_for(lambda x: isinstance(x, ArraySpec)) @property def _pt_aliases(self) -> Iterable[Alias]: @@ -144,6 +169,10 @@ def _pt_structs(self) -> Iterable[tuple[str, Struct]]: 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()) + @property def _pt_structs_and_unions(self) -> Iterable[tuple[str, Struct | Union]]: return self._pt_filter_for_class(Struct | Union) diff --git a/packtype/utils/__init__.py b/packtype/utils/__init__.py index e2f69ea..813735f 100644 --- a/packtype/utils/__init__.py +++ b/packtype/utils/__init__.py @@ -5,8 +5,12 @@ from . import array, constant, enum, package, struct, union from .basic import ( clog2, + copy, + diff, + diff_table, get_doc, get_name, + get_package, get_source, get_width, is_scalar, @@ -19,9 +23,13 @@ "array", "clog2", "constant", + "copy", + "diff", + "diff_table", "enum", "get_doc", "get_name", + "get_package", "get_source", "get_width", "is_scalar", diff --git a/packtype/utils/basic.py b/packtype/utils/basic.py index 22d0c71..3359a6a 100644 --- a/packtype/utils/basic.py +++ b/packtype/utils/basic.py @@ -5,8 +5,10 @@ import inspect import math +import tabulate + from ..types.alias import Alias -from ..types.array import ArraySpec +from ..types.array import ArraySpec, PackedArray, UnpackedArray from ..types.assembly import PackedAssembly from ..types.base import Base from ..types.enum import Enum @@ -33,7 +35,7 @@ def get_width( :param ptype: The Packtype definition to inspect :return: The width in bits of the Packtype definition """ - if isinstance(ptype, PackedAssembly | Enum | NumericType | Union): + if isinstance(ptype, PackedAssembly | Enum | NumericType | Union | PackedArray | ArraySpec): return ptype._pt_width elif issubclass(ptype, PackedAssembly | Enum | NumericType | Union): return ptype._PT_WIDTH @@ -49,7 +51,19 @@ def get_name(ptype: type[Base] | Base) -> str: :param ptype: The Packtype definition to inspect :return: The name of the Packtype definition """ - if isinstance(ptype, ScalarType) or (inspect.isclass(ptype) and issubclass(ptype, ScalarType)): + # If an array instance is passed, unwrap it to get the spec + if isinstance(ptype, PackedArray | UnpackedArray): + ptype = ptype._pt_spec + # For an array spec... + if isinstance(ptype, ArraySpec): + # If it is associated to a package, use the type name it is declared with + if ptype._PT_ATTACHED_TO is not None: + return ptype._PT_ATTACHED_TO._PT_FIELDS[ptype] + # Otherwise, raise an exception + raise TypeError(f"Cannot determine a name for nested array spec {ptype}") + elif isinstance(ptype, ScalarType) or ( + inspect.isclass(ptype) and issubclass(ptype, ScalarType) + ): ptype = ptype if inspect.isclass(ptype) else type(ptype) return ptype._PT_ATTACHED_TO._PT_FIELDS[ptype] elif isinstance(ptype, Base) or (inspect.isclass(ptype) and issubclass(ptype, Base)): @@ -67,7 +81,9 @@ def get_package(ptype: type[Base] | Base) -> type[Base] | None: :param ptype: The Packtype definition to inspect :return: The Package to which this type is attached """ - if not isinstance(ptype, Base) and not (inspect.isclass(ptype) and issubclass(ptype, Base)): + if not isinstance(ptype, Base | ArraySpec) and not ( + inspect.isclass(ptype) and issubclass(ptype, Base) + ): raise TypeError(f"{ptype} is not a Packtype definition") return ptype._PT_ATTACHED_TO @@ -114,7 +130,7 @@ def is_signed(ptype: type[NumericType] | NumericType) -> bool: raise TypeError(f"{ptype} is not a Packtype definition") -def unpack(ptype: type[Base], value: int) -> Base: +def unpack[T: Base](ptype: type[T], value: int) -> T: """ Unpack a value into a Packtype definition :param ptype: The Packtype definition to unpack into @@ -146,6 +162,20 @@ def pack(pinst: Base) -> int: return int(pinst) +def copy[T: Base](pinst: T) -> T: + """ + Safely copy a Packtype instance + + This is also more performant than using copy.deepcopy() on the packtype objects. + + Use unpack and pack to create a new instance via the packed integer + """ + # Special case for arrays as type(pinst) is a Packed Array not an array spec + if isinstance(pinst, PackedArray): + return unpack(pinst._pt_spec, pack(pinst)) + return unpack(type(pinst), pack(pinst)) + + def is_scalar(ptype: type[Base] | Base) -> bool: """ Check if a Packtype definition is a scalar type @@ -155,3 +185,121 @@ def is_scalar(ptype: type[Base] | Base) -> bool: return isinstance(ptype, ScalarType) or ( inspect.isclass(ptype) and issubclass(ptype, ScalarType) ) + + +def diff_table(value_a: type[Base], value_b: type[Base], verbose: bool = False) -> str: + """ + Generate a diff between two Packtype instances. This is a recursive function + that walks through the hierarchy and compares all fields, tabulating where + differences occur. + + :param value_a: First item to be compared + :param value_b: Second item to be compared + :param verbose: Show all fields in complex objects and do not filter matching + objects (default: False) + :return: A tabulate table containing differing fields. If the objects are the + same an empty string is returned. This function should not be used + to check the equality of two objects as it is orders of magnitude + slower than the __eq__ operation added to all Numeric types + """ + # Check that the values are the same type + if not isinstance(value_b, type(value_a)): + raise TypeError("Value A and Value B must be the same class") + if value_a == value_b: + return "" + diff_struct = diff(value_a, value_b, verbose) + return tabulate.tabulate( + tabular_data=diff_struct, + headers="keys", + tablefmt="grid", + colalign=("left", "center", "center", "center"), + ) + + +def _format_value(value: type[Base] | Base) -> str: + """ + Format a Packtype value for display in diffs + + :param value: The Packtype value to format + :return: The formatted string + """ + if isinstance(value, Enum): + return str(value) + else: + # Max digits is enough to show 64 bits in hex before truncating + max_digits = 21 + int_value = int(value) + int_str = str(int_value) + # Get top digits of int + int_str = int_str[:max_digits] + ("..." if len(int_str) > max_digits else "") + hex_str = f"0x{int_value:_X}" + # Get top digits of hex value + hex_str = hex_str[:max_digits] + ("..." if len(hex_str) > max_digits else "") + return f"{int_str}\n{hex_str}" + + +def diff( + value_a: type[Base], value_b: type[Base], verbose: bool = False, _path: list[str] | None = None +) -> dict[str, list[str | int | Enum]]: + """ + A recursive function to generate a diff between two Packtype instances. + :param value_a: Item to be compared + :param value_b: Other item to be compared + :param verbose: Show all fields in complex objects and do not filter matching objects + :param _path: A list of strings that can be joined with a '.' to represent the current + field path in the recursion + :return: A dictionary containing fields of: Member name, Value A, Value B and "Diff" + """ + # Check that the values are the same type + diff_dict = {"Member name": [], "Value A": [], "Value B": [], "Diff": []} + + if value_a.__class__ != value_b.__class__: + raise TypeError( + "Value A and Value B must be the same class," + f" got {value_a.__class__} and {value_b.__class__}" + ) + # Initialize the name string to the class name if not provided + _path = _path[:] if _path is not None else [get_name(value_a)] + # Return early if the values are the same and not verbose diff + if value_a == value_b and not verbose: + return diff_dict + # Add the components to the diff entry + diff_dict["Member name"].append(".".join(_path)) + diff_dict["Value A"].append(_format_value(value_a)) + diff_dict["Value B"].append(_format_value(value_b)) + diff_dict["Diff"].append("Y" if value_a != value_b else " ") + # Recurse to get subfields for complex types + if isinstance(value_a, PackedAssembly | Union): + # Complex assembly comparision + for (a_subfield, a_name), b_subfield in zip( + value_a._pt_fields.items(), + value_b._pt_fields.keys(), + strict=False, + ): + if a_subfield != b_subfield or verbose: + sub_diff = diff( + a_subfield, + b_subfield, + verbose, + _path=[*_path, a_name], + ) + for key in diff_dict.keys(): + diff_dict[key].extend(sub_diff[key]) + elif isinstance(value_a, (PackedArray | UnpackedArray)): + if len(value_a) != len(value_b): + raise ValueError("Cannot diff arrays of different lengths") + # Array comparison + for idx, (a_element, b_element) in enumerate(zip(value_a, value_b, strict=False)): + if a_element != b_element or verbose: + element_path = _path.copy() + element_path[-1] += f"[{idx}]" + sub_diff = diff( + a_element, + b_element, + verbose, + _path=element_path, + ) + for key in diff_dict.keys(): + diff_dict[key].extend(sub_diff[key]) + + return diff_dict diff --git a/packtype/utils/enum.py b/packtype/utils/enum.py index 017cb61..2124fbb 100644 --- a/packtype/utils/enum.py +++ b/packtype/utils/enum.py @@ -2,14 +2,23 @@ # SPDX-License-Identifier: Apache-2.0 # +import inspect from collections.abc import Iterable from ..types.enum import Enum -from ..types.package import Package -def _normalise_enum(enum: Enum | type[Enum]) -> Package: - assert isinstance(enum, Enum) or issubclass(enum, Enum) +def is_enum(enum: Enum | type[Enum]) -> bool: + """ + Check if a Packtype definition is an enum. + :param enum: The Packtype definition to inspect + :return: True if the definition is an enum, False otherwise + """ + return isinstance(enum, Enum) or (inspect.isclass(enum) and issubclass(enum, Enum)) + + +def _normalise_enum(enum: Enum | type[Enum]) -> Enum: + assert is_enum(enum), "Input must be an Enum or subclass thereof." if not isinstance(enum, Enum): enum = enum() return enum diff --git a/packtype/utils/package.py b/packtype/utils/package.py index 04d1234..d861481 100644 --- a/packtype/utils/package.py +++ b/packtype/utils/package.py @@ -4,10 +4,11 @@ from collections.abc import Iterable +from ..types.array import ArraySpec from ..types.constant import Constant from ..types.enum import Enum from ..types.package import Package -from ..types.scalar import Scalar +from ..types.scalar import ScalarType from ..types.struct import Struct from ..types.union import Union from .basic import get_name @@ -43,7 +44,18 @@ def get_constants(pkg: Package | type[Package]) -> Iterable[tuple[str, Constant] return pkg._pt_constants -def get_scalars(pkg: Package | type[Package]) -> Iterable[tuple[str, type[Scalar]]]: +def get_all_types( + pkg: Package | type[Package], +) -> Iterable[tuple[str, type[ScalarType | Enum | Struct | Union | ArraySpec]]]: + """ + Get the scalars defined in a Packtype package. + :return: Iterable of tuples of the scalar name and definition + """ + pkg = _normalise_package(pkg) + return pkg._pt_all_types + + +def get_scalars(pkg: Package | type[Package]) -> Iterable[tuple[str, type[ScalarType]]]: """ Get the scalars defined in a Packtype package. :return: Iterable of tuples of the scalar name and definition diff --git a/packtype/utils/struct.py b/packtype/utils/struct.py index 120276b..cc1216a 100644 --- a/packtype/utils/struct.py +++ b/packtype/utils/struct.py @@ -2,26 +2,34 @@ # SPDX-License-Identifier: Apache-2.0 # +import inspect + from ..types.base import Base from ..types.scalar import ScalarType from ..types.struct import Struct from .basic import get_name -def _normalise_struct(struct: Struct | type[Struct]) -> Struct: - assert isinstance(struct, Struct) or issubclass(struct, Struct) - if not isinstance(struct, Struct): - struct = struct() - return struct - - def is_struct(ptype: type[Base] | Base) -> bool: """ Check if a Packtype definition is a struct. :param ptype: The Packtype definition to check :return: True if the definition is a struct, False otherwise """ - return isinstance(ptype, Struct) or issubclass(ptype, Struct) + return isinstance(ptype, Struct) or (inspect.isclass(ptype) and issubclass(ptype, Struct)) + + +def _normalise_struct(inst_or_type: Struct | type[Struct]) -> Struct: + """ + Utility functions may be called with a type instance or the type definition, + and certain operations require an instance. This function ensures that the + input is 'normalised' to be an instance. + + :param struct: The struct instance or struct type definition + :return: A struct instance + """ + assert is_struct(inst_or_type), "Input must be a Struct or subclass thereof." + return inst_or_type if isinstance(inst_or_type, Struct) else inst_or_type() def get_fields_msb_desc(struct: Struct | type[Struct]) -> list[tuple[int, int, tuple[str, Base]]]: diff --git a/packtype/utils/union.py b/packtype/utils/union.py index 359eae1..5cdcd37 100644 --- a/packtype/utils/union.py +++ b/packtype/utils/union.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 # +import inspect from collections.abc import Iterable from ..types.base import Base @@ -10,20 +11,26 @@ from .basic import get_name -def _normalise_union(union: Union | type[Union]) -> Union: - assert isinstance(union, Union) or issubclass(union, Union) - if not isinstance(union, Union): - union = union() - return union - - def is_union(ptype: type[Base] | Base) -> bool: """ Check if a Packtype definition is a union. :param ptype: The Packtype definition to check :return: True if the definition is a union, False otherwise """ - return isinstance(ptype, Union) or issubclass(ptype, Union) + return isinstance(ptype, Union) or (inspect.isclass(ptype) and issubclass(ptype, Union)) + + +def _normalise_union(inst_or_type: Union | type[Union]) -> Union: + """ + Utility functions may be called with a type instance or the type definition, + and certain operations require an instance. This function ensures that the + input is 'normalised' to be an instance. + + :param union: The union instance or union type definition + :return: A union instance + """ + assert is_union(inst_or_type), "Input must be a Union or subclass thereof." + return inst_or_type if isinstance(inst_or_type, Union) else inst_or_type() def get_members(union: Union | type[Union]) -> Iterable[tuple[str, Base]]: diff --git a/pyproject.toml b/pyproject.toml index 771ac70..8f55577 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,14 +4,14 @@ build-backend = "poetry.masonry.api" [tool.poetry] name = "packtype" -version = "3.0.3" +version = "3.1.0" description = "Packed data structure specifications for multi-language hardware projects" authors = ["Peter Birch "] license = "Apache-2.0" readme = "README.md" [tool.poetry.dependencies] -python = ">=3.11,<4.0" +python = ">=3.12,<4.0" click = "^8.1.7" Mako = "^1.3.0" rich = "^13.7.0" @@ -19,6 +19,7 @@ typing-extensions = "^4.8.0" svg-py = "^1.4.3" ordered-set = "^4.1.0" lark = "^1.2.2" +tabulate = "^0.9.0" [tool.poetry.group.dev] optional = true @@ -60,7 +61,7 @@ addopts = [ shell = "poetry run pytest" [tool.ruff] -target-version = "py311" +target-version = "py312" line-length = 100 [tool.ruff.lint] diff --git a/tests/grammar/test_descriptions.py b/tests/grammar/test_descriptions.py index 43ac11c..b9e8249 100644 --- a/tests/grammar/test_descriptions.py +++ b/tests/grammar/test_descriptions.py @@ -3,7 +3,8 @@ # from packtype.grammar import parse_string -from tests.fixtures import reset_registry + +from ..fixtures import reset_registry assert reset_registry diff --git a/tests/grammar/test_enum.py b/tests/grammar/test_enum.py index 70b65ca..b2bccff 100644 --- a/tests/grammar/test_enum.py +++ b/tests/grammar/test_enum.py @@ -7,7 +7,8 @@ from packtype.grammar import ParseError, parse_string from packtype.types.enum import Enum, EnumError, EnumMode from packtype.types.wrap import BadAttributeError -from packtype.utils import get_width +from packtype.utils import get_width, pack, unpack +from packtype.utils.basic import copy from ..fixtures import reset_registry @@ -288,3 +289,38 @@ def test_parse_enum_bad_modifier(): """ ) ) + + +def test_parse_enum_copy(): + """Test copying an enum instance.""" + pkg = next( + parse_string( + """ + package the_package { + enum my_enum { + A + B + C + } + } + """ + ) + ) + # For known enum values, copy returns the singleton instance + inst = unpack(pkg.my_enum, 0x1) + inst_copy = copy(inst) + + assert isinstance(inst_copy, pkg.my_enum) + assert pack(inst_copy) == pack(inst) + assert inst_copy is inst # Enum values are singletons + assert inst_copy == pkg.my_enum.B + + # For unknown enum values, copy creates a new instance + inst_unknown = unpack(pkg.my_enum, 0x3) # 3 is within 2-bit range but not A/B/C + inst_unknown_copy = copy(inst_unknown) + + assert isinstance(inst_unknown_copy, pkg.my_enum) + assert pack(inst_unknown_copy) == pack(inst_unknown) + assert inst_unknown_copy is not inst_unknown # Unknown values are not singletons + assert inst_unknown_copy._pt_bv.value == inst_unknown._pt_bv.value + assert inst_unknown_copy._pt_bv is not inst_unknown._pt_bv diff --git a/tests/grammar/test_normative.py b/tests/grammar/test_normative.py new file mode 100644 index 0000000..02798d2 --- /dev/null +++ b/tests/grammar/test_normative.py @@ -0,0 +1,56 @@ +# Copyright 2023-2025, Peter Birch, mailto:peter@intuity.io +# SPDX-License-Identifier: Apache-2.0 +# + +import pytest + +from packtype.grammar import ParseError, parse_string +from packtype.types.normative import Priority + +from ..fixtures import reset_registry + +assert reset_registry + + +def test_parse_norm(): + """Test that normative point attributes are correctly set.""" + pkg = next( + parse_string( + """ + package the_package { + vnorm 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 pkg.my_feature._PT_ATTRIBUTES["priority"] == Priority.P1 + + +def test_parse_erroneous_priority(): + """Test that priority typo missing index is caught.""" + with pytest.raises(ParseError, match="Failed to parse input on line 3"): + next( + parse_string( + """ + package the_package { + vnorm my_feature : P "This feature is high priority" + } + """ + ) + ) + + +def test_parse_priority_typo(): + """Check that a typo for unsupported Priority is caught""" + with pytest.raises(KeyError, match="P5"): + next( + parse_string( + """ + package the_package { + vnorm my_feature : P5 "This feature's priority is wrong!" + } + """ + ) + ) diff --git a/tests/grammar/test_scalar.py b/tests/grammar/test_scalar.py index 93d2398..95a3b1d 100644 --- a/tests/grammar/test_scalar.py +++ b/tests/grammar/test_scalar.py @@ -6,7 +6,8 @@ from packtype.grammar import ParseError, parse_string from packtype.types.scalar import ScalarType -from packtype.utils import get_width +from packtype.utils import get_width, pack, unpack +from packtype.utils.basic import copy from ..fixtures import reset_registry @@ -56,3 +57,25 @@ def test_parse_scalar_bad_assign(): """ ) ) + + +def test_parse_scalar_copy(): + """Test copying a scalar instance.""" + pkg = next( + parse_string( + """ + package the_package { + my_scalar: scalar[8] + } + """ + ) + ) + inst = unpack(pkg.my_scalar, 0x7B) + inst_copy = copy(inst) + + assert isinstance(inst_copy, ScalarType) + assert inst_copy == inst + assert inst_copy is not inst + assert pack(inst_copy) == pack(inst) + assert inst_copy._pt_bv.value == inst._pt_bv.value + assert inst_copy._pt_bv is not inst._pt_bv diff --git a/tests/grammar/test_struct.py b/tests/grammar/test_struct.py index 0fbb18b..c680edb 100644 --- a/tests/grammar/test_struct.py +++ b/tests/grammar/test_struct.py @@ -9,7 +9,8 @@ from packtype.grammar import ParseError, UnknownEntityError, parse_string from packtype.types.assembly import Packing, WidthError from packtype.types.struct import Struct -from packtype.utils import get_width, unpack +from packtype.utils import get_width, pack, unpack +from packtype.utils.basic import copy from ..fixtures import reset_registry @@ -282,3 +283,31 @@ def test_parse_struct_nested(): assert inst[1].e[1].c[1].b == (the_value >> 38) & 0b11 assert inst[1].e[1].d == (the_value >> 40) & 0b11 assert inst[1].f == (the_value >> 42) & 0b11 + + +def test_parse_struct_copy(): + """Test copying a struct instance.""" + pkg = next( + parse_string( + """ + package the_package { + struct my_struct { + a: scalar[4] + b: scalar[4] + } + } + """ + ) + ) + inst = unpack(pkg.my_struct, 0x48) + inst_copy = copy(inst) + + assert isinstance(inst_copy, pkg.my_struct) + assert pack(inst_copy) == pack(inst) + assert inst_copy is not inst + assert inst_copy._pt_bv is not inst._pt_bv + + # Verify independence by mutating copy + inst_copy.a = 0x1 + assert inst.a == 0x8 + assert inst_copy.a == 0x1 diff --git a/tests/grammar/test_union.py b/tests/grammar/test_union.py index b05658d..99b1dfa 100644 --- a/tests/grammar/test_union.py +++ b/tests/grammar/test_union.py @@ -6,7 +6,8 @@ from packtype.grammar import UnknownEntityError, parse_string from packtype.types.union import Union, UnionError -from packtype.utils import get_width +from packtype.utils import get_width, pack, unpack +from packtype.utils.basic import copy from ..fixtures import reset_registry @@ -121,3 +122,28 @@ def test_parse_union_bad_field_ref(): """ ) ) + + +def test_parse_union_copy(): + """Test copying a union instance.""" + pkg = next( + parse_string( + """ + package the_package { + union my_union { + a: scalar[8] + b: scalar[8] + } + } + """ + ) + ) + inst = unpack(pkg.my_union, 0x42) + inst_copy = copy(inst) + + assert isinstance(inst_copy, pkg.my_union) + assert pack(inst_copy) == pack(inst) + assert inst_copy is not inst + assert inst_copy._pt_bv is not inst._pt_bv + assert inst_copy.a == 0x42 + assert inst_copy.b == 0x42 diff --git a/tests/grammar/test_variant.py b/tests/grammar/test_variant.py new file mode 100644 index 0000000..83c4210 --- /dev/null +++ b/tests/grammar/test_variant.py @@ -0,0 +1,241 @@ +# Copyright 2023-2025, Peter Birch, mailto:peter@intuity.io +# SPDX-License-Identifier: Apache-2.0 +# + +import pytest + +from packtype.grammar import parse_string +from packtype.grammar.declarations import VariantError +from packtype.grammar.transformer import TransformerError +from packtype.utils import get_width + +from ..fixtures import reset_registry + +assert reset_registry + + +def test_parse_variant(): + """Parse package with variants using different configurations""" + pkg_def = """ + package the_package { + A : constant = 1 + + variants { + default { + B : constant = A + 2 // 3 + C : constant = B + 3 // 6 + } + other { + B : constant = A + 10 // 11 + C : constant = B + 20 // 31 + } + } + + type_a_t : scalar[A] + + variants { + default { + type_x_t : scalar[B] + } + another { + type_y_t : scalar[C] + } + } + } + """ + # Default pathway + pkg_default = next(parse_string(pkg_def)) + assert len(pkg_default._PT_FIELDS) == 5 + assert int(pkg_default.A) == 1 + assert int(pkg_default.B) == 3 + assert int(pkg_default.C) == 6 + assert get_width(pkg_default.type_a_t) == 1 + assert get_width(pkg_default.type_x_t) == 3 + assert not hasattr(pkg_default, "type_y_t") + # Unused variants (also follows default pathway) + pkg_default = next(parse_string(pkg_def, variants=["blah", "blergh"])) + assert len(pkg_default._PT_FIELDS) == 5 + assert int(pkg_default.A) == 1 + assert int(pkg_default.B) == 3 + assert int(pkg_default.C) == 6 + assert get_width(pkg_default.type_a_t) == 1 + assert get_width(pkg_default.type_x_t) == 3 + assert not hasattr(pkg_default, "type_y_t") + # Enable 'other' + pkg_default = next(parse_string(pkg_def, variants=["X", "Y", "other"])) + assert len(pkg_default._PT_FIELDS) == 5 + assert int(pkg_default.A) == 1 + assert int(pkg_default.B) == 11 + assert int(pkg_default.C) == 31 + assert get_width(pkg_default.type_a_t) == 1 + assert get_width(pkg_default.type_x_t) == 11 + assert not hasattr(pkg_default, "type_y_t") + # Enable 'other' and 'another' + pkg_default = next(parse_string(pkg_def, variants=["X", "Y", "other", "another"])) + assert len(pkg_default._PT_FIELDS) == 5 + assert int(pkg_default.A) == 1 + assert int(pkg_default.B) == 11 + assert int(pkg_default.C) == 31 + assert get_width(pkg_default.type_a_t) == 1 + assert not hasattr(pkg_default, "type_x_t") + assert get_width(pkg_default.type_y_t) == 31 + + +def test_parse_variant_missing_default(): + """Check an error is raised when a variant can't be resolved""" + pkg_def = """ + package the_package { + variants { + some_cond { + A : constant = 123 + } + } + } + """ + with pytest.raises(TransformerError) as excinfo: + next(parse_string(pkg_def)) + assert "Variant at N/A:3 is missing a default case." in str(excinfo.value) + + +def test_parse_variant_multiple_default(): + """Check an error is raised when multiple variants are marked as default""" + pkg_def = """ + package the_package { + variants { + default { + A : constant = 123 + } + default { + B : constant = 456 + } + other { + C : constant = 789 + } + } + } + """ + with pytest.raises(VariantError) as excinfo: + next(parse_string(pkg_def)) + assert "Multiple default variants defined" in str(excinfo.value) + + +def test_parse_variant_complex_condition(): + """Check that condition parsing works as expected""" + pkg_def = """ + package the_package { + variants { + a and b { + A : constant = 1 + } + default {} + } + variants { + c and d { + B : constant = 2 + } + default {} + } + variants { + a or b { + C : constant = 3 + } + default {} + } + variants { + c or d { + D : constant = 4 + } + default {} + } + variants { + a and b or c and d { + E : constant = 5 + } + default {} + } + variants { + a or b and c or d { + F : constant = 6 + } + default {} + } + } + """ + # With nothing defined, we should not see anything + pkg = next(parse_string(pkg_def)) + for field in ("A", "B", "C", "D", "E", "F"): + assert not hasattr(pkg, field) + # With a and c we should get C, D, F + pkg = next(parse_string(pkg_def, variants=["a", "c"])) + for field in ("C", "D", "F"): + assert hasattr(pkg, field) + for field in ("A", "B", "E"): + assert not hasattr(pkg, field) + # With b and d we should get C, D, F + pkg = next(parse_string(pkg_def, variants=["b", "d"])) + for field in ("C", "D", "F"): + assert hasattr(pkg, field) + for field in ("A", "B", "E"): + assert not hasattr(pkg, field) + # With a and b we should get A, C, E, F + pkg = next(parse_string(pkg_def, variants=["a", "b"])) + for field in ("A", "C", "E", "F"): + assert hasattr(pkg, field) + for field in ("B", "D"): + assert not hasattr(pkg, field) + # With c and d we should get B, D, E, F + pkg = next(parse_string(pkg_def, variants=["c", "d"])) + for field in ("B", "D", "E", "F"): + assert hasattr(pkg, field) + for field in ("A", "C"): + assert not hasattr(pkg, field) + # With b and d we should get C, D, F + pkg = next(parse_string(pkg_def, variants=["b", "d"])) + for field in ("C", "D", "F"): + assert hasattr(pkg, field) + for field in ("A", "B", "E"): + assert not hasattr(pkg, field) + + +def test_parse_variant_first_match(): + """Check that only the first matching variant is taken""" + pkg_def = """ + package the_package { + variants { + default { + X : constant = 1 + } + a { + A : constant = 2 + } + b { + B : constant = 3 + } + } + } + """ + # With nothing defined, we should only see X + pkg = next(parse_string(pkg_def)) + assert pkg.X == 1 + for field in ("A", "B"): + assert not hasattr(pkg, field) + # With 'a' we should only see A + pkg = next(parse_string(pkg_def, variants=["a"])) + assert pkg.A == 2 + for field in ("X", "B"): + assert not hasattr(pkg, field) + # With 'a', 'b' we should only see A + pkg = next(parse_string(pkg_def, variants=["a", "b"])) + assert pkg.A == 2 + for field in ("X", "B"): + assert not hasattr(pkg, field) + # With 'a', 'b' we should only see A (regardless of variant order on CLI) + pkg = next(parse_string(pkg_def, variants=["b", "a"])) + assert pkg.A == 2 + for field in ("X", "B"): + assert not hasattr(pkg, field) + # With 'b' we should only see B + pkg = next(parse_string(pkg_def, variants=["b"])) + assert pkg.B == 3 + for field in ("X", "A"): + assert not hasattr(pkg, field) diff --git a/tests/integration/test_sv.py b/tests/integration/test_sv.py index 7ea8e98..f655a8d 100644 --- a/tests/integration/test_sv.py +++ b/tests/integration/test_sv.py @@ -3,6 +3,7 @@ # import subprocess +import sys from pathlib import Path resources = Path(__file__).parent.absolute() / "resources" @@ -12,7 +13,7 @@ def test_sv(tmp_path): # Wrap around the CLI result = subprocess.run( ( - "python3", + sys.executable, "-m", "packtype", "--debug", @@ -34,7 +35,7 @@ def test_sv_only(tmp_path): # Wrap around the CLI result = subprocess.run( ( - "python3", + sys.executable, "-m", "packtype", "--debug", diff --git a/tests/pysyntax/test_array.py b/tests/pysyntax/test_array.py index 49fc691..a7c886e 100644 --- a/tests/pysyntax/test_array.py +++ b/tests/pysyntax/test_array.py @@ -7,6 +7,7 @@ import packtype from packtype import Constant, Packing, Scalar +from packtype.utils import pack, unpack from ..fixtures import reset_registry @@ -289,3 +290,109 @@ class TestUnion: assert int(inst) == data_a for x, y in itertools.product(range(4), range(3)): assert inst.member_a[x][y] == (data_a >> ((x * 3 * 2) + (y * 2))) & 0b11 + + +def test_array_struct_comparison(): + """Test that arrays of structs and unions can be compared""" + + @packtype.package() + class TestPkg: + a_nibble: Scalar[4] + + @TestPkg.struct() + class ExampleStruct: + nibble_a: TestPkg.a_nibble + nibble_b: Scalar[4] + byte_a: Scalar[8] + + @TestPkg.union() + class ExampleUnion: + raw: Scalar[16] + struct: ExampleStruct + + @TestPkg.struct() + class UnwrappedStructArray: + idx_0: TestPkg.ExampleStruct + idx_1: TestPkg.ExampleStruct + idx_2: TestPkg.ExampleStruct + idx_3: TestPkg.ExampleStruct + + @packtype.package() + class TestPkg2: + StructArray: TestPkg.ExampleStruct[4] + UnionArray: TestPkg.ExampleUnion[4] + + test_value = 0x0123456789ABCDEF # 64 bit value to pack + value_a = unpack(TestPkg.UnwrappedStructArray, test_value) + value_b = unpack(TestPkg2.StructArray, test_value) + assert pack(value_a) == pack(value_b) + assert value_a.idx_0 == value_b[0] + assert value_a.idx_1 == value_b[1] + assert value_a.idx_2 == value_b[2] + assert value_a.idx_3 == value_b[3] + + value_c = unpack(TestPkg2.StructArray, test_value) + value_d = unpack(TestPkg2.UnionArray, test_value) + assert value_d == value_c + + +def test_array_struct_str(): + """Test that arrays of structs print correctly""" + + @packtype.package() + class TestPkg: + a_nibble: Scalar[4] + + @TestPkg.struct() + class ExampleStruct: + nibble_a: TestPkg.a_nibble + nibble_b: Scalar[4] + byte_a: Scalar[8] + + @TestPkg.union() + class ExampleUnion: + raw: Scalar[16] + struct: ExampleStruct + + @packtype.package() + class TestPkg2: + StructArray: TestPkg.ExampleStruct[4] + UnionArray: TestPkg.ExampleUnion[2] + + test_value = 0x0123456789ABCDEF # 64 bit value to pack + value = unpack(TestPkg2.StructArray, test_value) + assert str(value) == ( + f"PackedArray - 4 entries: 0x{test_value:X}\n" + f"- Entry[0]: ExampleStruct: 0x{0xCDEF:X}\n" + f" [15: 8] ├─ byte_a = 0xCD\n" + f" [ 7: 4] ├─ nibble_b = 0xE\n" + f" [ 3: 0] └─ nibble_a = 0xF\n" + f"- Entry[1]: ExampleStruct: 0x{0x89AB:X}\n" + f" [15: 8] ├─ byte_a = 0x89\n" + f" [ 7: 4] ├─ nibble_b = 0xA\n" + f" [ 3: 0] └─ nibble_a = 0xB\n" + f"- Entry[2]: ExampleStruct: 0x{0x4567:X}\n" + f" [15: 8] ├─ byte_a = 0x45\n" + f" [ 7: 4] ├─ nibble_b = 0x6\n" + f" [ 3: 0] └─ nibble_a = 0x7\n" + f"- Entry[3]: ExampleStruct: 0x{0x123:X}\n" + f" [15: 8] ├─ byte_a = 0x01\n" + f" [ 7: 4] ├─ nibble_b = 0x2\n" + f" [ 3: 0] └─ nibble_a = 0x3" + ) + + assert str(unpack(TestPkg2.UnionArray, 0x01234567)) == ( + f"PackedArray - 2 entries: 0x{0x01234567:X}\n" + f"- Entry[0]: ExampleUnion: 0x{0x4567:X} (union):\n" + f" |- raw -> Unsigned Scalar[16]: 0x{0x4567:X}\n" + f" |- struct -> ExampleStruct: 0x{0x4567:X}\n" + f" [15: 8] ├─ byte_a = 0x45\n" + f" [ 7: 4] ├─ nibble_b = 0x6\n" + f" [ 3: 0] └─ nibble_a = 0x7\n" + f"- Entry[1]: ExampleUnion: 0x{0x123:X} (union):\n" + f" |- raw -> Unsigned Scalar[16]: 0x{0x123:04X}\n" + f" |- struct -> ExampleStruct: 0x{0x123:X}\n" + f" [15: 8] ├─ byte_a = 0x01\n" + f" [ 7: 4] ├─ nibble_b = 0x2\n" + f" [ 3: 0] └─ nibble_a = 0x3" + ) diff --git a/tests/pysyntax/test_struct.py b/tests/pysyntax/test_struct.py index 6fb3034..d5a1b4b 100644 --- a/tests/pysyntax/test_struct.py +++ b/tests/pysyntax/test_struct.py @@ -101,10 +101,10 @@ class TestStruct: assert inst.ef.value == (ef_value := 39) assert str(inst) == ( - f"TestStruct: 0x{value:06X}\n" - f" |- [11: 0] ab = 0x{ab_value:03X}\n" - f" |- [14:12] cd = 0x{cd_value:01X}\n" - f" |- [23:15] ef = 0x{ef_value:03X}" + f"TestStruct: 0x{value:X}\n" + f" [23:15] ├─ ef = 0x{ef_value:03X}\n" + f" [14:12] ├─ cd = 0x{cd_value:01X}\n" + f" [11: 0] └─ ab = 0x{ab_value:03X}" ) @@ -384,3 +384,49 @@ class TestStruct: TestStruct(ab=123, cd=[4, 5, 6], ef=41, gh=3) assert str(e.value) == "TestStruct does not contain a field called 'gh'" + + +def test_struct_tree_printing_max_depth(): + """Test tree printing max_depth parameter with nested structures""" + + @packtype.package() + class TestPkg: + pass + + @TestPkg.struct() + class Level3: + a: Scalar[4] + b: Scalar[4] + + @TestPkg.struct() + class Level2: + x: Scalar[8] + nested: Level3 + + @TestPkg.struct() + class Level1: + y: Scalar[8] + nested: Level2 + + @TestPkg.struct() + class Level0: + z: Scalar[8] + nested: Level1 + + inst = Level0._pt_unpack(0x12345678) + + # max_depth=1: only top level (2 fields) + output1 = inst._str_tree(max_depth=1) + assert "Level0: 0x12345678" in output1 + assert output1.count("├─") + output1.count("└─") == 2 # Only 2 top-level fields + + # max_depth=2: top level + one level (2 + 2 = 4 fields total) + output2 = inst._str_tree(max_depth=2) + assert "Level0: 0x12345678" in output2 + assert output2.count("├─") + output2.count("└─") == 4 # 2 top + 2 nested + + # max_depth=0: unlimited (all 4 levels: 2+2+2+2 = 8 fields) + output0 = inst._str_tree(max_depth=0) + assert "Level0: 0x12345678" in output0 + assert output0.count("├─") + output0.count("└─") == 8 # All fields visible + assert "│ │" in output0 # Should have 3-level nesting indicators diff --git a/tests/pysyntax/test_union.py b/tests/pysyntax/test_union.py index 145df90..4b2155b 100644 --- a/tests/pysyntax/test_union.py +++ b/tests/pysyntax/test_union.py @@ -153,13 +153,13 @@ class Packet: assert int(inst.header.flags) == hdr_flags_val assert str(inst) == ( - f"Packet: 0x{value:08X} (union):\n" - f" |- raw -> Unsigned Scalar[32]: 0x{value:08X}\n" - f" |- header -> Header: 0x{value:08X}\n" - f" |- [15: 0] address = 0x{hdr_addr_val:04X}\n" - f" |- [23:16] length = 0x{hdr_len_val:02X}\n" - f" |- [27:24] mode = 0x{hdr_mode_val:01X}\n" - f" |- [31:28] flags = 0x{hdr_flags_val:01X}" + f"Packet: 0x{value:X} (union):\n" + f" |- raw -> Unsigned Scalar[32]: 0x{value:X}\n" + f" |- header -> Header: 0x{value:X}\n" + f" [31:28] ├─ flags = 0x{hdr_flags_val:01X}\n" + f" [27:24] ├─ mode = 0x{hdr_mode_val:01X}\n" + f" [23:16] ├─ length = 0x{hdr_len_val:02X}\n" + f" [15: 0] └─ address = 0x{hdr_addr_val:04X}" ) diff --git a/tests/svg/test_struct_svg.py b/tests/svg/test_struct_svg.py index 9f828d3..0936d4f 100644 --- a/tests/svg/test_struct_svg.py +++ b/tests/svg/test_struct_svg.py @@ -3,6 +3,7 @@ # import subprocess +import sys from pathlib import Path import packtype @@ -96,7 +97,7 @@ def test_struct_svg_command(tmp_path): # Wrap around the CLI result = subprocess.run( ( - "python3", + sys.executable, "-m", "packtype", "svg", diff --git a/tests/utils/test_utils_basic.py b/tests/utils/test_utils_basic.py index 2ca9e51..ffa5a6d 100644 --- a/tests/utils/test_utils_basic.py +++ b/tests/utils/test_utils_basic.py @@ -5,6 +5,7 @@ import math import pytest +import tabulate import packtype from packtype import Constant, Scalar, utils @@ -22,10 +23,15 @@ def test_utils_basic_clog2(): def test_utils_basic_get_width(): @packtype.package() - class TestPkg: + class TestPkgA: simple_type: Scalar[8] - assert utils.get_width(TestPkg.simple_type) == 8 + @packtype.package() + class TestPkgB: + arrayed_type: TestPkgA.simple_type[4] + + assert utils.get_width(TestPkgA.simple_type) == 8 + assert utils.get_width(TestPkgB.arrayed_type) == 32 def test_utils_basic_get_name(): @@ -36,6 +42,24 @@ class TestPkg: assert utils.get_name(TestPkg.simple_type) == "simple_type" +def test_utils_bad_get_name(): + """Check that an error is raised for struct field name""" + + @packtype.package() + class TestPkg: + simple_type: Scalar[8] + + @TestPkg.struct() + class TestStruct: + field_a: TestPkg.simple_type[4] + + with pytest.raises(TypeError) as exc: + struct = TestStruct() + utils.get_name(struct.field_a) + + assert str(exc.value).startswith("Cannot determine a name for nested array spec") + + def test_utils_basic_get_doc(): @packtype.package() class TestPkg: @@ -167,3 +191,320 @@ class TestUnion: with pytest.raises(TypeError): utils.pack(TestUnion) + + +def test_utils_diff_scalar_table(): + """ + Test the diff tableing with the diff utility on scalars + """ + + @packtype.package() + class TestPkg: + sc_unsigned: Scalar[8] + sc_small: Scalar[4] + + @TestPkg.enum() + class TestEnum: + A: Constant = 0x1 + B: Constant = 0x2 + C: Constant = 0x3 + + a = TestPkg.sc_unsigned(42) + b = TestPkg.sc_unsigned(42) + c = TestPkg.sc_unsigned(84) + enum_a = TestEnum.A + enum_a2 = TestEnum.A + enum_b = TestEnum.B + + assert utils.diff_table(a, b) == "" + assert utils.diff_table(b, a) == "" + assert utils.diff_table(enum_a, enum_a2) == "" + diff_struct = { + "Member name": ["sc_unsigned"], + "Value A": [f"{42}\n0x{42:02X}"], + "Value B": [f"{84}\n0x{84:02X}"], + "Diff": ["Y"], + } + assert utils.diff(a, c) == diff_struct + diff_struct_enum = { + "Member name": ["TestEnum"], + "Value A": [f"{enum_a}"], + "Value B": [f"{enum_b}"], + "Diff": ["Y"], + } + assert utils.diff(enum_a, enum_b) == diff_struct_enum + # Check table can be properly rendered + assert utils.diff_table(enum_a, enum_b) == tabulate.tabulate( + diff_struct_enum, + headers="keys", + tablefmt="grid", + colalign=("left", "center", "center", "center"), + ) + + +def test_utils_diff_struct(): + """Test the diff function with structs.""" + + @packtype.package() + class TestPkg: + sc_unsigned: Scalar[8] + + @TestPkg.struct() + class TestStruct: + field_a: TestPkg.sc_unsigned + field_b: TestPkg.sc_unsigned + + @TestPkg.struct() + class TestStructOuter: + inner: TestStruct + field_c: TestPkg.sc_unsigned + + a = TestStruct( + field_a=TestPkg.sc_unsigned(1), + field_b=TestPkg.sc_unsigned(2), + ) + b = TestStruct( + field_a=TestPkg.sc_unsigned(1), + field_b=TestPkg.sc_unsigned(3), + ) + c = TestStructOuter( + inner=b, + field_c=TestPkg.sc_unsigned(4), + ) + d = TestStructOuter( + inner=a, + field_c=TestPkg.sc_unsigned(5), + ) + a_values = ["0x201", "0x2"] + b_values = ["0x301", "0x3"] + diff_ab = { + "Member name": ["TestStruct", "TestStruct.field_b"], + "Value A": [f"{int(v, 16)}\n{v}" for v in a_values], + "Value B": [f"{int(v, 16)}\n{v}" for v in b_values], + "Diff": ["Y", "Y"], + } + assert utils.diff(a, b) == diff_ab + + a_values = ["0x4_0301", "0x301", "0x3", "0x4"] + b_values = ["0x5_0201", "0x201", "0x2", "0x5"] + diff_cd = { + "Member name": [ + "TestStructOuter", + "TestStructOuter.inner", + "TestStructOuter.inner.field_b", + "TestStructOuter.field_c", + ], + "Value A": [f"{int(v, 16)}\n{v}" for v in a_values], + "Value B": [f"{int(v, 16)}\n{v}" for v in b_values], + "Diff": ["Y", "Y", "Y", "Y"], + } + # Check that tables can be properly rendered + assert utils.diff(c, d) == diff_cd + assert utils.diff_table(c, d) == tabulate.tabulate( + diff_cd, headers="keys", tablefmt="grid", colalign=("left", "center", "center", "center") + ) + + +def test_utils_diff_arrays(): + """Test the diff function with arrays of structs.""" + + @packtype.package() + class TestPkg: + nibble: Scalar[4] + + @TestPkg.struct() + class TestByte: + low: TestPkg.nibble + high: TestPkg.nibble + + @packtype.package() + class TestPkg2: + StructArray4: TestPkg.TestByte[4] + MDimStructArray2x2: TestPkg.TestByte[2][2] + + a = utils.unpack(TestPkg2.StructArray4, 0x12_34_56_78) + b = utils.unpack(TestPkg2.StructArray4, 0x12_34_56_79) + + a_values = ["0x1234_5678", "0x78", "0x8"] + b_values = ["0x1234_5679", "0x79", "0x9"] + diff_struct = { + "Member name": ["StructArray4", "StructArray4[0]", "StructArray4[0].low"], + "Value A": [f"{int(v, 16)}\n{v}" for v in a_values], + "Value B": [f"{int(v, 16)}\n{v}" for v in b_values], + "Diff": ["Y", "Y", "Y"], + } + assert utils.diff(a, b) == diff_struct + + a2d = utils.unpack(TestPkg2.MDimStructArray2x2, 0x12_34_56_78) + b2d = utils.unpack(TestPkg2.MDimStructArray2x2, 0x13_34_57_78) + a_values = ["0x1234_5678", "0x5678", "0x56", "0x6", "0x1234", "0x12", "0x2"] + b_values = ["0x1334_5778", "0x5778", "0x57", "0x7", "0x1334", "0x13", "0x3"] + diff_struct_2d = { + "Member name": [ + "MDimStructArray2x2", + "MDimStructArray2x2[0]", + "MDimStructArray2x2[0][1]", + "MDimStructArray2x2[0][1].low", + "MDimStructArray2x2[1]", + "MDimStructArray2x2[1][1]", + "MDimStructArray2x2[1][1].low", + ], + "Value A": [f"{int(v, 16)}\n{v}" for v in a_values], + "Value B": [f"{int(v, 16)}\n{v}" for v in b_values], + "Diff": ["Y", "Y", "Y", "Y", "Y", "Y", "Y"], + } + + assert utils.diff(a2d, b2d) == diff_struct_2d + # Check that tables can be properly rendered + assert utils.diff_table(a2d, b2d) == tabulate.tabulate( + diff_struct_2d, + headers="keys", + tablefmt="grid", + colalign=("left", "center", "center", "center"), + ) + + +def test_utils_diff_unions(): + """Test the diff function with unions.""" + + @packtype.package() + class TestPkg: + nibble: Scalar[4] + + @TestPkg.struct() + class TestByte: + low: TestPkg.nibble + high: TestPkg.nibble + + @TestPkg.union() + class TestUnion: + struct: TestByte + pair: TestPkg.nibble[2] + raw: Scalar[8] + + a = utils.unpack(TestUnion, 0x12) + b = utils.unpack(TestUnion, 0x13) + a_values = ["0x12", "0x12", "0x2", "0x12", "0x2", "0x12"] + b_values = ["0x13", "0x13", "0x3", "0x13", "0x3", "0x13"] + diff_struct = { + "Member name": [ + "TestUnion", + "TestUnion.struct", + "TestUnion.struct.low", + "TestUnion.pair", + "TestUnion.pair[0]", + "TestUnion.raw", + ], + "Value A": [f"{int(v, 16)}\n{v}" for v in a_values], + "Value B": [f"{int(v, 16)}\n{v}" for v in b_values], + "Diff": ["Y", "Y", "Y", "Y", "Y", "Y"], + } + assert utils.diff(a, b) == diff_struct + + +def test_copy_enum(): + """ + Test 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_copy = utils.copy(enum_a) + assert enum_a == enum_a_copy + enum_a += 1 + assert enum_a != enum_a_copy + + +def test_copy_struct(): + """ + Test that the util function 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_copy = utils.copy(struct_a) + struct_a_equals = struct_a + assert struct_a == struct_a_copy == struct_a_copy + # Mutate original struct a + struct_a.low = 0x0 + assert struct_a_equals == struct_a + assert struct_a_copy != struct_a + + +def test_copy_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_copy = utils.copy(union_a) + union_a_equals = union_a + assert union_a == union_a_copy == union_a_copy + # Mutate original struct a + union_a.struct.low = 0x0 + assert union_a_equals == union_a + assert union_a_copy != union_a + + +def test_copy_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_copy = utils.copy(byte_array) + 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) + assert byte_2d_array == byte_2d_array_copy == 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_copy + + assert byte_2d_array == byte_2d_array_equals + assert byte_2d_array_copy != byte_2d_array diff --git a/vscode/packtype/syntaxes/packtype.tmLanguage.json b/vscode/packtype/syntaxes/packtype.tmLanguage.json index 94fedd2..0363fbf 100644 --- a/vscode/packtype/syntaxes/packtype.tmLanguage.json +++ b/vscode/packtype/syntaxes/packtype.tmLanguage.json @@ -1,170 +1,176 @@ { - "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", - "name": "Packtype", - "patterns": [ - { - "match": "\\b(package|registers)\\s+([\\w]+)\\b", - "captures": { - "1": { "name": "keyword.control.root" }, - "2": { "name": "entity.name.type.packtype" } - } - }, - { - "match": "\\b(import)\\s+(\\w+)::(\\w+)\\b", - "captures": { - "1": { "name": "keyword.control.import" }, - "2": { "name": "entity.name.type.packtype" }, - "3": { "name": "entity.name.type.packtype" } - } - }, - { - "match": "\\b(\\w+)\\s*:\\s*(signed|unsigned)?\\s*(constant)\\s*(?:\\[\\s*(\\w+)\\s*\\])?\\s*=\\s*(.*?)$", - "captures": { - "1": { "name": "entity.name.type.packtype" }, - "2": { "name": "storage.modifier.packtype" }, - "3": { "name": "storage.type.packtype" }, - "4": { - "patterns": [ - { - "name": "constant.numeric.packtype", - "match": "\\b(\\d+|0[xX][a-fA-F0-9_]+)\\b" - }, - { - "name": "entity.name.type.packtype", - "match": "\\b(\\w+)\\b" - } - ] - }, - "5": { - "patterns": [ - { - "name": "constant.numeric.packtype", - "match": "\\b(\\d+|0[xX][a-fA-F0-9_]+)\\b" - }, - { - "name": "entity.name.type.packtype", - "match": "\\b(\\w+)\\b" - } - ] - } - } - }, - { - "match": "\\b(\\w+)\\s*:\\s*(signed|unsigned)?\\s*(scalar)\\s*(?:\\[\\s*(\\w+)\\s*\\])?", - "captures": { - "1": { "name": "entity.name.type.packtype" }, - "2": { "name": "storage.modifier.packtype" }, - "3": { "name": "storage.type.packtype" }, - "4": { - "patterns": [ - { - "name": "constant.numeric.packtype", - "match": "\\b(\\d+)\\b" - }, - { - "name": "entity.name.type.packtype", - "match": "\\b(\\w+)\\b" - } - ] - } - } - }, - { - "match": "\\b(enum)\\s+(onehot|gray|indexed)?\\s*(?:\\[\\s*(\\w+)\\s*\\])?\\s*(\\w+)\\b", - "captures": { - "1": { "name": "storage.type.packtype" }, - "2": { "name": "storage.modifier.packtype" }, - "3": { - "patterns": [ - { - "name": "constant.numeric.packtype", - "match": "\\b(\\d+)\\b" - }, - { - "name": "entity.name.type.packtype", - "match": "\\b(\\w+)\\b" - } - ] - }, - "4": { "name": "entity.name.type.packtype" } - } - }, - { - "match": "\\b(struct)\\s+(msb|lsb|from_msb|from_lsb)?\\s*(?:\\[\\s*(\\w+)\\s*\\])?\\s*(\\w+)\\b", - "captures": { - "1": { "name": "storage.type.packtype" }, - "2": { "name": "storage.modifier.packtype" }, - "3": { - "patterns": [ - { - "name": "constant.numeric.packtype", - "match": "\\b(\\d+)\\b" - }, - { - "name": "entity.name.type.packtype", - "match": "\\b(\\w+)\\b" - } - ] - }, - "4": { "name": "entity.name.type.packtype" } - } - }, - { - "match": "\\b(\\w+)\\s*:\\s*(\\w+)\\b", - "captures": { - "1": { "name": "entity.name.type.packtype" }, - "2": { "name": "entity.name.type.packtype" } - } - }, - { - "match": "\\b(union)\\s+(\\w+)\\b", - "captures": { - "1": { "name": "storage.type.packtype" }, - "2": { "name": "entity.name.type.packtype" } - } - }, - { - "match": "//.*$", - "name": "comment.line.double-slash.packtype" - }, - { - "match": "\\b(0x[0-9a-fA-F]+|\\d+)\\b", - "name": "constant.numeric.packtype" - }, - { - "match": "\\b(\\w+)\\b", - "name": "entity.name.type.packtype" - }, - { - "include": "#docstrings" - }, - { - "include": "#strings" - } - ], - "repository": { - "docstrings": { - "name": "string.quoted.docstring.packtype", - "begin": "\"\"\"", - "end": "\"\"\"", - "patterns": [ - { - "name": "constant.character.escape.packtype", - "match": "\\\\." - } - ] - }, - "strings": { - "name": "string.quoted.double.packtype", - "begin": "\"", - "end": "\"", - "patterns": [ - { - "name": "constant.character.escape.packtype", - "match": "\\\\." - } - ] - } - }, - "scopeName": "source.packtype" + "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", + "name": "Packtype", + "patterns": [ + { + "match": "\\b(package|registers)\\s+([\\w]+)\\b", + "captures": { + "1": { "name": "keyword.control.root" }, + "2": { "name": "entity.name.type.packtype" } + } + }, + { + "match": "\\b(import)\\s+(\\w+)::(\\w+)\\b", + "captures": { + "1": { "name": "keyword.control.import" }, + "2": { "name": "entity.name.type.packtype" }, + "3": { "name": "entity.name.type.packtype" } + } + }, + { + "match": "\\b(variants|default)\\s+", + "captures": { + "1": { "name": "keyword.control.variants" } + } + }, + { + "match": "\\b(\\w+)\\s*:\\s*(signed|unsigned)?\\s*(constant)\\s*(?:\\[\\s*(\\w+)\\s*\\])?\\s*=\\s*(.*?)$", + "captures": { + "1": { "name": "entity.name.type.packtype" }, + "2": { "name": "storage.modifier.packtype" }, + "3": { "name": "storage.type.packtype" }, + "4": { + "patterns": [ + { + "name": "constant.numeric.packtype", + "match": "\\b(\\d+|0[xX][a-fA-F0-9_]+)\\b" + }, + { + "name": "entity.name.type.packtype", + "match": "\\b(\\w+)\\b" + } + ] + }, + "5": { + "patterns": [ + { + "name": "constant.numeric.packtype", + "match": "\\b(\\d+|0[xX][a-fA-F0-9_]+)\\b" + }, + { + "name": "entity.name.type.packtype", + "match": "\\b(\\w+)\\b" + } + ] + } + } + }, + { + "match": "\\b(\\w+)\\s*:\\s*(signed|unsigned)?\\s*(scalar)\\s*(?:\\[\\s*(\\w+)\\s*\\])?", + "captures": { + "1": { "name": "entity.name.type.packtype" }, + "2": { "name": "storage.modifier.packtype" }, + "3": { "name": "storage.type.packtype" }, + "4": { + "patterns": [ + { + "name": "constant.numeric.packtype", + "match": "\\b(\\d+)\\b" + }, + { + "name": "entity.name.type.packtype", + "match": "\\b(\\w+)\\b" + } + ] + } + } + }, + { + "match": "\\b(enum)\\s+(onehot|gray|indexed)?\\s*(?:\\[\\s*(\\w+)\\s*\\])?\\s*(\\w+)\\b", + "captures": { + "1": { "name": "storage.type.packtype" }, + "2": { "name": "storage.modifier.packtype" }, + "3": { + "patterns": [ + { + "name": "constant.numeric.packtype", + "match": "\\b(\\d+)\\b" + }, + { + "name": "entity.name.type.packtype", + "match": "\\b(\\w+)\\b" + } + ] + }, + "4": { "name": "entity.name.type.packtype" } + } + }, + { + "match": "\\b(struct)\\s+(msb|lsb|from_msb|from_lsb)?\\s*(?:\\[\\s*(\\w+)\\s*\\])?\\s*(\\w+)\\b", + "captures": { + "1": { "name": "storage.type.packtype" }, + "2": { "name": "storage.modifier.packtype" }, + "3": { + "patterns": [ + { + "name": "constant.numeric.packtype", + "match": "\\b(\\d+)\\b" + }, + { + "name": "entity.name.type.packtype", + "match": "\\b(\\w+)\\b" + } + ] + }, + "4": { "name": "entity.name.type.packtype" } + } + }, + { + "match": "\\b(\\w+)\\s*:\\s*(\\w+)\\b", + "captures": { + "1": { "name": "entity.name.type.packtype" }, + "2": { "name": "entity.name.type.packtype" } + } + }, + { + "match": "\\b(union)\\s+(\\w+)\\b", + "captures": { + "1": { "name": "storage.type.packtype" }, + "2": { "name": "entity.name.type.packtype" } + } + }, + { + "match": "//.*$", + "name": "comment.line.double-slash.packtype" + }, + { + "match": "\\b(0x[0-9a-fA-F]+|\\d+)\\b", + "name": "constant.numeric.packtype" + }, + { + "match": "\\b(\\w+)\\b", + "name": "entity.name.type.packtype" + }, + { + "include": "#docstrings" + }, + { + "include": "#strings" + } + ], + "repository": { + "docstrings": { + "name": "string.quoted.docstring.packtype", + "begin": "\"\"\"", + "end": "\"\"\"", + "patterns": [ + { + "name": "constant.character.escape.packtype", + "match": "\\\\." + } + ] + }, + "strings": { + "name": "string.quoted.double.packtype", + "begin": "\"", + "end": "\"", + "patterns": [ + { + "name": "constant.character.escape.packtype", + "match": "\\\\." + } + ] + } + }, + "scopeName": "source.packtype" }