diff --git a/docs/syntax/instance.md b/docs/syntax/instance.md new file mode 100644 index 0000000..367db5d --- /dev/null +++ b/docs/syntax/instance.md @@ -0,0 +1,81 @@ +Instances of types can be associated to a package, allowing structured constant +values to be declared. + +## Example + +The Packtype definition can either use a Python dataclass style or the Packtype +custom grammar: + +=== "Python (.py)" + + ```python linenums="1" + import packtype + from packtype import Constant, Scalar + + @packtype.package() + class MyPackage: + pass + + @MyPackage.enum() + class Month: + JAN : Constant + FEB : Constant + ... + DEC : Constant + + @MyPackage.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)) + ``` + +=== "Packtype (.pt)" + + ```sv linenums="1" + package my_package { + enum month_e { + JAN : constant + FEB : constant + ... + DEC : constant + } + + struct date_t { + year : scalar[12] + month : month_e + day : scalar[5] + } + + SUMMER_START : month_e = month_e::JUN + SUMMER_END : month_e = month_e::AUG + CHRISTMAS : date_t = { + year = 2025 + month = month_e::DEC + day = 25 + } + } + ``` + +As rendered to SystemVerilog: + +```sv linenums="1" +package my_package; + +localparam month_e SUMMER_START = month_e'(7); + +localparam month_e SUMMER_END = month_e'(9); + +// CHRISTMAS +localparam date_t CHRISTMAS = '{ + day: 5'h19 + , month: month_e'(11) + , year: 12'h7E9 +}; + +endpackage : my_package +``` diff --git a/examples/constants/spec.pt b/examples/constants/spec.pt new file mode 100644 index 0000000..2aee189 --- /dev/null +++ b/examples/constants/spec.pt @@ -0,0 +1,52 @@ +package date_consts { + + // Basic definitions + DAYS_PER_YEAR : constant = 365 + DAYS_PER_WEEK : constant = 7 + HOURS_PER_DAY : constant[8] = 24 + MINS_PER_HOUR : constant = 60 + + // Weekdays + enum weekday_e { + MON : constant + TUE : constant + WED : constant + THU : constant + FRI : constant + SAT : constant + SUN : constant + } + + START_OF_WEEK : weekday_e = weekday_e::MON + END_OF_WEEK : weekday_e = weekday_e::SUN + + // Months + enum month_e { + JAN : constant + FEB : constant + MAR : constant + APR : constant + MAY : constant + JUN : constant + JUL : constant + AUG : constant + SEP : constant + OCT : constant + NOV : constant + DEC : constant + } + + // Date + struct date_t { + year : scalar[12] + month : month_e + day : scalar[5] + } + + CHRISTMAS : date_t = { + year = 2025 + month = month_e::DEC + day = 25 + } + +} diff --git a/examples/constants/spec.py b/examples/constants/spec.py index 08d1e8b..bff91fa 100644 --- a/examples/constants/spec.py +++ b/examples/constants/spec.py @@ -3,7 +3,7 @@ # import packtype -from packtype import Constant +from packtype import Constant, Scalar @packtype.package() @@ -14,3 +14,42 @@ class DateConsts: DAYS_PER_WEEK: Constant = 7 HOURS_PER_DAY: Constant[8] = 24 MINS_PER_HOUR: Constant = 60 + + +@DateConsts.enum() +class Weekday: + MON: Constant + TUE: Constant + WED: Constant + THU: Constant + FRI: Constant + SAT: Constant + SUN: Constant + + +@DateConsts.enum() +class Month: + JAN: Constant + FEB: Constant + MAR: Constant + APR: Constant + MAY: Constant + JUN: Constant + JUL: Constant + AUG: Constant + SEP: Constant + OCT: Constant + NOV: Constant + DEC: Constant + + +@DateConsts.struct() +class Date: + year: Scalar[12] + month: Month + day: Scalar[5] + + +DateConsts._pt_attach_instance("START_OF_WEEK", Weekday.MON) +DateConsts._pt_attach_instance("END_OF_WEEK", Weekday.SUN) +DateConsts._pt_attach_instance("CHRISTMAS", Date(year=2025, month=Month.DEC, day=25)) diff --git a/examples/constants/test.sh b/examples/constants/test.sh index 3d994ba..067bb7b 100755 --- a/examples/constants/test.sh +++ b/examples/constants/test.sh @@ -11,4 +11,7 @@ this_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" export PYTHONPATH=${this_dir}/../..:$PYTHONPATH # Invoke packtype -python3 -m packtype --debug code package sv ${this_dir}/out spec.py +python3 -m packtype --debug code package sv ${this_dir}/out_py spec.py + +# Invoke packtype on Packtype syntax +python3 -m packtype --debug code package sv --type-filter none ${this_dir}/out_pt ${this_dir}/spec.pt diff --git a/mkdocs.yml b/mkdocs.yml index d2dfdae..a69c1e1 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -38,6 +38,7 @@ nav: - Arrays: syntax/arrays.md - Constants: syntax/constant.md - Enumerations: syntax/enum.md + - Instances: syntax/instance.md - Packages: syntax/package.md - Scalars: syntax/scalar.md - Structs: syntax/struct.md diff --git a/packtype/common/expression.py b/packtype/common/expression.py index 8604cb2..fef7bd4 100644 --- a/packtype/common/expression.py +++ b/packtype/common/expression.py @@ -82,6 +82,10 @@ def evaluate(self, cb_lookup: Callable[[str], int]) -> int: # Check if the LHS is a _PT_BASE attribute elif hasattr(lhs, "_PT_BASE") and type(lhs).__name__ != "Constant": return lhs + # Check if the LHS is a foreign-reference (supports enum references) + elif type(lhs).__name__ == "ForeignRef": + f_type = cb_lookup(lhs.package) + return int(getattr(f_type, lhs.name)) # Otherwise, cast LHS to an integer else: return int(lhs) diff --git a/packtype/grammar/declarations.py b/packtype/grammar/declarations.py index e3da28e..d34124a 100644 --- a/packtype/grammar/declarations.py +++ b/packtype/grammar/declarations.py @@ -6,6 +6,7 @@ from dataclasses import dataclass from pathlib import Path +from .. import utils from ..common.expression import Expression from ..types.alias import Alias from ..types.array import ArraySpec @@ -80,7 +81,7 @@ def resolve( if isinstance(raw_dim, Expression): eval_dims.append(raw_dim.evaluate(cb_resolve)) else: - raise Exception("Unexpected width type in DeclScalar") + raise ValueError("Unexpected width type in DeclScalar") return eval_dims @@ -143,6 +144,52 @@ def to_instance( return const +@dataclass +class FieldAssignment: + field: str + value: Expression + + +@dataclass() +class FieldAssignments: + assignments: list[FieldAssignment] + + +@dataclass() +class DeclInstance: + position: Position + name: str + ref: str + assignment: Expression | FieldAssignments + description: Description | None = None + + def to_instance( + self, + cb_resolve: Callable[ + [ + str, + ], + int | type[Base], + ], + ) -> Base: + # Get the referenced type + ref = cb_resolve(self.ref) + # If the assignment is a simple expression, referenced type needs to be + # either a scalar or an enum + if isinstance(self.assignment, Expression): + if not issubclass(ref, Scalar | Enum): + raise TypeError(f"{ref} must be a scalar or enum for simple expression assignment") + return utils.unpack(ref, self.assignment.evaluate(cb_resolve)) + # If instead we get a field assigment, referenced type needs to be a + # struct or union + elif isinstance(self.assignment, FieldAssignments): + if not issubclass(ref, Struct | Union): + raise TypeError(f"{ref} must be a struct or union for field assignment") + return ref( + **{x.field: x.value.evaluate(cb_resolve) for x in self.assignment.assignments} + ) + + @dataclass() class DeclScalar: position: Position diff --git a/packtype/grammar/grammar.py b/packtype/grammar/grammar.py index 6d99409..12ba463 100644 --- a/packtype/grammar/grammar.py +++ b/packtype/grammar/grammar.py @@ -20,6 +20,7 @@ DeclConstant, DeclEnum, DeclImport, + DeclInstance, DeclPackage, DeclScalar, DeclStruct, @@ -135,6 +136,8 @@ def _resolve(ref: str | ForeignRef) -> int: match decl: # Imports case DeclImport(): + # Check for name collisions + _check_collision(decl.foreign.name) # Resolve the package if (foreign_pkg := namespaces.get(decl.foreign.package, None)) is None: raise ImportError(f"Unknown package '{decl.foreign.package}'") @@ -144,8 +147,6 @@ def _resolve(ref: str | ForeignRef) -> int: f"'{decl.foreign.name}' not declared in package " f"'{decl.foreign.package}'" ) - # Check for name collisions - _check_collision(decl.foreign.name) # Remember this type if isinstance(foreign_type, Constant): known_entities[decl.foreign.name] = (foreign_type, decl.position) @@ -153,22 +154,24 @@ def _resolve(ref: str | ForeignRef) -> int: known_entities[decl.foreign.name] = (foreign_type, decl.position) # Aliases case DeclAlias(): + # Check for name collisions + _check_collision(decl.name) + # Attach to the package package._pt_attach( alias := decl.to_class(_resolve), name=decl.name, ) - # Check for name collisions - _check_collision(decl.name) # Remember this type known_entities[decl.name] = (alias, decl.position) # Build constants case DeclConstant(): + # Check for name collisions + _check_collision(decl.name) + # Attach to the package constant = decl.to_instance(_resolve) if keep_expression: constant._PT_EXPRESSION = decl.expr package._pt_attach_constant(decl.name, constant) - # Check for name collisions - _check_collision(decl.name) # Check for a constant override if decl.name in constant_overrides: get_log().debug( @@ -178,21 +181,34 @@ def _resolve(ref: str | ForeignRef) -> int: constant._pt_set(int(constant_overrides[decl.name])) # Remember this constant known_entities[decl.name] = (constant, decl.position) + # Build instances (constants that reference other types) + case DeclInstance(): + # Check for name collisions + _check_collision(decl.name) + # Attach to the package + package._pt_attach_instance( + decl.name, + inst := decl.to_instance(_resolve), + ) + # Remember this type + known_entities[decl.name] = (inst, decl.position) # Build aliases and scalars case DeclScalar() | DeclAlias(): + # Check for name collisions + _check_collision(decl.name) + # Attach to the package package._pt_attach( obj := decl.to_class(_resolve), name=decl.name, ) - # Check for name collisions - _check_collision(decl.name) # Remember this type known_entities[decl.name] = (obj, decl.position) # Build enums, structs, and unions case DeclEnum() | DeclStruct() | DeclUnion(): - package._pt_attach(obj := decl.to_class(source, _resolve)) # Check for name collisions _check_collision(decl.name) + # Attach to the package + package._pt_attach(obj := decl.to_class(source, _resolve)) # Remember this type known_entities[decl.name] = (obj, decl.position) case _: diff --git a/packtype/grammar/packtype.lark b/packtype/grammar/packtype.lark index 64a80bb..86a5ebf 100644 --- a/packtype/grammar/packtype.lark +++ b/packtype/grammar/packtype.lark @@ -82,7 +82,7 @@ decl_import: "import" foreign_ref // ============================================================================= // Example: local_type_t : foreign_type_t -decl_alias: name ":" (name | foreign_ref) dimensions? descr? +decl_alias: name ":" (name | foreign_ref) dimensions? ("=" (expr | field_assignments))? descr? // Example: MY_CONSTANT : constant[8] = 123 decl_constant: name ":" "constant"i dimension? "=" expr descr? @@ -90,6 +90,9 @@ decl_constant: name ":" "constant"i dimension? "=" expr descr? // Example: simple_type_t : scalar[8] decl_scalar: name ":" (signed|unsigned)? "scalar"i dimensions? descr? +field_assignment: name "=" expr descr? +?field_assignments: "{" field_assignment* "}" + // ============================================================================= // Enumerations // @@ -166,6 +169,7 @@ decl_union: "union"i name "{" descr? modifier* field* "}" expr: expr_term (OPERATOR expr_term)* ?expr_term: name + | foreign_ref | HEX | BINARY | DECIMAL diff --git a/packtype/grammar/transformer.py b/packtype/grammar/transformer.py index f3a89ba..8a46515 100644 --- a/packtype/grammar/transformer.py +++ b/packtype/grammar/transformer.py @@ -17,11 +17,14 @@ DeclEnum, DeclField, DeclImport, + DeclInstance, DeclPackage, DeclScalar, DeclStruct, DeclUnion, Description, + FieldAssignment, + FieldAssignments, ForeignRef, Modifier, Position, @@ -96,13 +99,33 @@ def dimensions(self, body): def foreign_ref(self, body): return ForeignRef(*body) + def field_assignment(self, body): + return FieldAssignment(*body) + + def field_assignments(self, body): + return FieldAssignments(assignments=body) + @v_args(meta=True) def decl_import(self, meta, body): return DeclImport(Position(meta.line, meta.column), *body) @v_args(meta=True) def decl_alias(self, meta, body): - return DeclAlias(Position(meta.line, meta.column), *body) + name = body.pop(0) + ref = body.pop(0) + dimensions = body.pop(0) if body and isinstance(body[0], DeclDimensions) else None + if body and isinstance(body[0], Expression | FieldAssignments): + return DeclInstance( + Position(meta.line, meta.column), + name, + ref, + body[0], + body[1] if len(body) > 1 else None, + ) + else: + return DeclAlias( + Position(meta.line, meta.column), name, ref, dimensions, body[0] if body else None + ) @v_args(meta=True) def decl_constant(self, meta, body): diff --git a/packtype/templates/package.sv.mako b/packtype/templates/package.sv.mako index a5a2a80..5de888d 100644 --- a/packtype/templates/package.sv.mako +++ b/packtype/templates/package.sv.mako @@ -166,7 +166,33 @@ typedef union packed { %endif %endfor +// ============================================================================= +// Instances +// ============================================================================= + +%for name, inst in baseline._pt_instances: +// ${name | filters.constant} +localparam ${utils.get_name(inst) | filters.type} ${name | filters.constant} = \ + %if isinstance(inst, Struct): +'{ +<% first = True %>\ + %for _msb, _lsb, (name, field) in utils.struct.get_fields_msb_desc(inst): +<% value = int(field) %>\ + ${" " if first else ","} ${name}: \ + %if isinstance(field, ScalarType): +${utils.get_width(field)}'h${f"{value:X}"} + %else: +${utils.get_name(field) | filters.type}'(${value}) + %endif +<% first = False %>\ + %endfor +}\ + %else: +${utils.get_name(inst) | filters.type}'(${f"{int(inst):X}"})\ + %endif +; +%endfor endpackage : ${baseline._pt_name() | filters.package} /* verilator lint_on UNUSEDPARAM */ diff --git a/packtype/types/package.py b/packtype/types/package.py index ca7c72b..27c5f28 100644 --- a/packtype/types/package.py +++ b/packtype/types/package.py @@ -41,6 +41,13 @@ def _pt_attach_constant(cls, fname: str, finst: Constant) -> Constant: cls._PT_FIELDS[finst] = fname return finst + @classmethod + def _pt_attach_instance(cls, fname: str, finst: Base) -> Base: + setattr(cls, fname, finst) + finst._PT_ATTACHED_TO = cls + cls._PT_FIELDS[finst] = fname + return finst + @classmethod def _pt_attach(cls, field: type[Base], name: str | None = None) -> Base: cls._PT_ATTACH.append(field) @@ -95,9 +102,17 @@ def _pt_fields(self) -> dict: return self._PT_FIELDS @property - def _pt_constants(self) -> Iterable[Constant]: + def _pt_constants(self) -> Iterable[tuple[str, Constant]]: return ((y, x) for x, y in self._pt_fields.items() if isinstance(x, Constant)) + @property + def _pt_instances(self) -> Iterable[tuple[str, Base]]: + return ( + (y, x) + for x, y in self._pt_fields.items() + 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) diff --git a/packtype/utils/basic.py b/packtype/utils/basic.py index 89c6e3f..1437ec8 100644 --- a/packtype/utils/basic.py +++ b/packtype/utils/basic.py @@ -48,7 +48,10 @@ 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, Base) or issubclass(ptype, Base): + if 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)): return ptype._pt_name() elif issubclass(ptype, Alias): return get_name(ptype._PT_ALIAS) @@ -56,6 +59,18 @@ def get_name(ptype: type[Base] | Base) -> str: raise TypeError(f"{ptype} is not a Packtype definition") +def get_package(ptype: type[Base] | Base) -> type[Base] | None: + """ + Get the package a Packtype definition is attached to, if the type is not + associated to a package then None will be returned + :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)): + raise TypeError(f"{ptype} is not a Packtype definition") + return ptype._PT_ATTACHED_TO + + def get_doc(ptype: type[Base] | Base) -> str: """ Get the docstring of a Packtype definition diff --git a/pyproject.toml b/pyproject.toml index b5d7d9f..771ac70 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.masonry.api" [tool.poetry] name = "packtype" -version = "3.0.2" +version = "3.0.3" description = "Packed data structure specifications for multi-language hardware projects" authors = ["Peter Birch "] license = "Apache-2.0" diff --git a/tests/grammar/test_instances.py b/tests/grammar/test_instances.py new file mode 100644 index 0000000..3d424ba --- /dev/null +++ b/tests/grammar/test_instances.py @@ -0,0 +1,88 @@ +# Copyright 2023-2025, Peter Birch, mailto:peter@intuity.io +# SPDX-License-Identifier: Apache-2.0 +# + + +from packtype.grammar import parse_string + +from ..fixtures import reset_registry + +assert reset_registry + + +def test_parse_enum_instance(): + """Parse an instance of an enum within a package""" + pkg = next( + parse_string( + """ + package the_package { + enum fruit_e { + APPLE : constant + ORANGE : constant + PEAR : constant + BANANA : constant + } + + BEST_FRUIT : fruit_e = fruit_e::APPLE + WORST_FRUIT : fruit_e = fruit_e::BANANA + } + """ + ) + ) + + assert isinstance(pkg.BEST_FRUIT, pkg.fruit_e) + assert pkg.BEST_FRUIT is pkg.fruit_e.APPLE + assert isinstance(pkg.WORST_FRUIT, pkg.fruit_e) + assert pkg.WORST_FRUIT is pkg.fruit_e.BANANA + + +def test_parse_struct_instance(): + """Parse an instance of a struct within a package""" + pkg = next( + parse_string( + """ + package the_package { + enum month_e { + JAN : constant + FEB : constant + MAR : constant + APR : constant + MAY : constant + JUN : constant + JUL : constant + AUG : constant + SEP : constant + OCT : constant + NOV : constant + DEC : constant + } + + struct date_t { + year : scalar[16] + month : month_e + day : scalar[5] + } + + CHRISTMAS : date_t = { + year = 2025 + month = month_e::DEC + day = 25 + } + + NEW_YEAR : date_t = { + year = 2026 + month = month_e::JAN + day = 1 + } + } + """ + ) + ) + assert isinstance(pkg.CHRISTMAS, pkg.date_t) + assert pkg.CHRISTMAS.year == 2025 + assert pkg.CHRISTMAS.month == pkg.month_e.DEC + assert pkg.CHRISTMAS.day == 25 + assert isinstance(pkg.NEW_YEAR, pkg.date_t) + assert pkg.NEW_YEAR.year == 2026 + assert pkg.NEW_YEAR.month == pkg.month_e.JAN + assert pkg.NEW_YEAR.day == 1