-
Notifications
You must be signed in to change notification settings - Fork 20
Consistent and readable generics #449
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
linusheck
wants to merge
26
commits into
stormchecker:dev
Choose a base branch
from
linusheck:generic-types
base: dev
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
cf4ac32
Migrate dft and pars to experimental new generic handling
linusheck ebd0e6b
Use custom deduction functions instead of just deducing from named ar…
linusheck 1e17a38
Remove pytest import
linusheck b2ed193
Format
linusheck a9419a5
Format again
linusheck 4e9a39a
Fix Copilot suggestions
linusheck 587c4cc
Update tests/dft/test_io.py
linusheck 1a46018
Update tests/dft/test_io.py
linusheck 197859d
Matthias: remove bindingTypeArgument
linusheck a93c6ad
Merge branch 'generic-types' of github.com:linusheck/stormpy into gen…
linusheck 9e52cce
Use classh everywhere
linusheck 6f41202
More informative error message for template instantiations
linusheck 95d11c5
Add test for invalid inputs
linusheck ba4de1f
BindingTypeArgument -> BindingValueTypeArgument
linusheck 1acf2ee
Update src/pars/model_instantiator.cpp
linusheck 770abdd
_parameters_by_type map, exact instance checks
linusheck e35e0ed
Merge branch 'generic-types' of github.com:linusheck/stormpy into gen…
linusheck 67f2e54
DFT simulator: use specialized functions for as_be and as_dependency
linusheck 1156bdc
Update lib/stormpy/_template.py
linusheck 9ccbedf
Update lib/stormpy/_template.py
linusheck 25accc9
Update lib/stormpy/_template.py
linusheck 8b385d1
Update lib/stormpy/_template.py
linusheck 9148e9b
Update lib/stormpy/_template.py
linusheck e37cc6f
autoformat
linusheck 8a644de
Merge branch 'master' of github.com:moves-rwth/stormpy into generic-t…
linusheck be245c1
Move generic types example to test_dft
linusheck File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,250 @@ | ||
| """Runtime representation and metadata for native C++ template families. | ||
|
|
||
| C++ template specializations still need to be compiled and bound separately. | ||
| ``TemplateClass`` groups those concrete Python classes behind one public, | ||
| subscriptable object and exposes their structure to documentation and stub | ||
| generation tools. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from collections.abc import Callable, Iterator, Mapping, Sequence | ||
| from dataclasses import dataclass | ||
| from types import MappingProxyType | ||
| from typing import Any, Literal, TypeAlias | ||
|
|
||
| TemplateParameterKind: TypeAlias = Literal["type", "value"] | ||
|
|
||
|
|
||
| @dataclass(frozen=True) | ||
| class TemplateParameter: | ||
| """Description of one public template parameter. | ||
|
|
||
| ``type`` parameters select a Python or native type. ``value`` parameters | ||
| select a runtime value, such as a ``ModelType`` enum member. | ||
| """ | ||
|
|
||
| name: str | ||
| kind: TemplateParameterKind = "type" | ||
|
|
||
|
|
||
| @dataclass(frozen=True) | ||
| class TemplateInstantiation: | ||
| """Description of one concrete native template specialization.""" | ||
|
|
||
| arguments: tuple[object, ...] | ||
| implementation: type | ||
| native_name: str | ||
|
|
||
|
|
||
| @dataclass(frozen=True) | ||
| class TemplateMetadata: | ||
| """Tooling-oriented description of a complete template family.""" | ||
|
|
||
| name: str | ||
| canonical_name: str | ||
| parameters: tuple[TemplateParameter, ...] | ||
| instantiations: tuple[TemplateInstantiation, ...] | ||
| deduction_guide: str | None | ||
|
|
||
|
|
||
| DeductionGuide: TypeAlias = Callable[["TemplateClass", tuple[Any, ...], Mapping[str, Any]], object] | ||
|
|
||
|
|
||
| def deduce_from_first_argument(source: "TemplateClass | None" = None, *, keyword: str | None = None) -> DeductionGuide: | ||
| """Create a guide that copies template arguments from an instance. | ||
|
|
||
| The first positional constructor argument is used when present. ``keyword`` | ||
| optionally identifies the same argument for keyword-only calls. If | ||
| ``source`` is omitted, the instance is matched against the family being | ||
| constructed; otherwise it is matched against ``source``. | ||
| """ | ||
|
|
||
| def deduction(family: TemplateClass, args: tuple[Any, ...], kwargs: Mapping[str, Any]) -> object: | ||
| if args: | ||
| instance = args[0] | ||
| elif keyword is not None and keyword in kwargs: | ||
| instance = kwargs[keyword] | ||
| else: | ||
| argument = f"{keyword!r}" if keyword is not None else " first positional" | ||
| raise TypeError(f"Cannot deduce template parameters without the {argument} argument") | ||
| return (source if source is not None else family).parameters_of(instance) | ||
|
|
||
| deduction.__name__ = "deduce_from_first_argument" | ||
| deduction.__qualname__ = "deduce_from_first_argument" | ||
|
volkm marked this conversation as resolved.
|
||
| return deduction | ||
|
|
||
|
|
||
| class TemplateClass: | ||
| """Native C++ template family exposed as a subscriptable Python object. | ||
|
|
||
| ``family[parameters]`` returns a registered concrete Python class. | ||
| ``family(*args, **kwargs)`` constructs a class selected by a configured | ||
| deduction guide. | ||
| """ | ||
|
|
||
| def __init__( | ||
| self, | ||
| canonical_name: str, | ||
| module: object, | ||
| *, | ||
| parameters: Sequence[str | TemplateParameter], | ||
| deduce: DeductionGuide | None = None, | ||
| ) -> None: | ||
| """Load a native template family. | ||
|
|
||
| :param canonical_name: Fully qualified public family name used for | ||
| native registration lookup, tooling, and representations. Its last | ||
| component is the native template-family name. | ||
| :param module: Native module containing the registered specializations. | ||
| :param parameters: Ordered descriptions or names of the public | ||
| template parameters. A bare name describes a type parameter. | ||
| :param deduce: Optional constructor deduction guide. It receives this | ||
| family, the positional constructor arguments, and a read-only | ||
| mapping of keyword arguments, and returns the complete template | ||
| parameter tuple. Deduction guides may inspect any constructor | ||
| argument and may provide defaults or transform parameters. | ||
| :raises RuntimeError: If the module has no registrations for ``name``, | ||
| has an empty registration table, or mixes parameter arities. | ||
| :raises ValueError: If parameter metadata or the canonical name is | ||
| invalid. | ||
| """ | ||
| canonical_module, separator, name = canonical_name.rpartition(".") | ||
| if not separator or not canonical_module or not name.isidentifier(): | ||
| raise ValueError("Canonical template name must be fully qualified") | ||
|
|
||
| try: | ||
| implementations = module._template_instantiations[name] | ||
| except (AttributeError, KeyError): | ||
| raise RuntimeError(f"Native module has no registrations for {name}") from None | ||
| if not implementations: | ||
| raise RuntimeError(f"Native module has no registrations for {name}") | ||
|
|
||
| arities = {len(arguments) for arguments in implementations} | ||
| if len(arities) != 1: | ||
| raise RuntimeError(f"Native module has inconsistent registrations for {name}") | ||
|
|
||
| parameter_metadata = tuple(parameter if isinstance(parameter, TemplateParameter) else TemplateParameter(parameter) for parameter in parameters) | ||
| arity = arities.pop() | ||
| if len(parameter_metadata) != arity: | ||
| raise ValueError(f"{name} has {arity} native template parameters, but {len(parameter_metadata)} parameter names were provided") | ||
| if any(not parameter.name or not parameter.name.isidentifier() for parameter in parameter_metadata): | ||
| raise ValueError(f"Invalid template parameter name for {name}") | ||
| if any(parameter.kind not in ("type", "value") for parameter in parameter_metadata): | ||
| raise ValueError(f"Invalid template parameter kind for {name}") | ||
| if len({parameter.name for parameter in parameter_metadata}) != arity: | ||
| raise ValueError(f"Template parameter names for {name} must be unique") | ||
|
|
||
| self.__name__ = name | ||
| self.__qualname__ = name | ||
| self.__module__ = canonical_module | ||
| self._canonical_name = canonical_name | ||
| self._parameters = parameter_metadata | ||
| self._arity = arity | ||
| self._deduction_guide = deduce | ||
| self._instantiations: dict[tuple[object, ...], type] = {} | ||
| self._parameters_by_type: dict[type, tuple[object, ...]] = {} | ||
| for arguments, implementation in implementations.items(): | ||
| self.register(arguments, implementation) | ||
|
|
||
| def _normalize(self, parameters: object) -> tuple[object, ...]: | ||
| """Convert subscription parameters to a validated tuple.""" | ||
| key = parameters if isinstance(parameters, tuple) else (parameters,) | ||
|
linusheck marked this conversation as resolved.
|
||
| if len(key) != self._arity: | ||
| raise TypeError(f"{self.__name__} expects {self._arity} template parameter{'s' if self._arity != 1 else ''}, got {len(key)}") | ||
| return key | ||
|
|
||
| def register(self, parameters: object, implementation: type) -> None: | ||
| """Add a concrete specialization to this family. | ||
|
|
||
| :param parameters: One parameter or a tuple containing the complete | ||
| template-parameter list. | ||
| :param implementation: Concrete Python class for those parameters. | ||
| :raises TypeError: If the parameter count is wrong or ``implementation`` | ||
| is not a class. | ||
| :raises ValueError: If the parameter tuple or implementation class is | ||
| already registered. | ||
| """ | ||
| key = self._normalize(parameters) | ||
| if not isinstance(implementation, type): | ||
| raise TypeError("A template implementation must be a class") | ||
| if key in self._instantiations: | ||
| raise ValueError(f"{self.__name__} instantiation for {key!r} is already registered") | ||
| if implementation in self._parameters_by_type: | ||
| other_key = self._parameters_by_type[implementation] | ||
| raise ValueError(f"{self.__name__} implementation {implementation!r} is already registered for {other_key!r}") | ||
| self._instantiations[key] = implementation | ||
|
linusheck marked this conversation as resolved.
|
||
| self._parameters_by_type[implementation] = key | ||
|
|
||
| def __getitem__(self, parameters: object) -> type: | ||
| """Return the concrete class registered for ``parameters``. | ||
|
|
||
| A single parameter may be written directly as ``family[T]``; multiple | ||
| parameters use normal subscription tuple syntax, ``family[T, U]``. | ||
|
|
||
| :raises TypeError: If the parameter count is wrong or no matching | ||
| specialization is registered. | ||
|
volkm marked this conversation as resolved.
|
||
| """ | ||
| key = self._normalize(parameters) | ||
| try: | ||
| return self._instantiations[key] | ||
| except KeyError: | ||
| raise TypeError(f"{self.__name__} has no instantiation for {key!r}") from None | ||
|
|
||
| def __call__(self, *args: Any, **kwargs: Any) -> Any: | ||
| """Construct the specialization selected by the deduction guide. | ||
|
|
||
| :raises TypeError: If no deduction guide is configured or the deduced | ||
| specialization is unavailable. | ||
| """ | ||
| if self._deduction_guide is None: | ||
| raise TypeError(f"{self.__name__} requires explicit template parameters") | ||
| parameters = self._deduction_guide(self, args, MappingProxyType(kwargs)) | ||
| return self[parameters](*args, **kwargs) | ||
|
|
||
| def parameters_of(self, instance: object) -> tuple[object, ...]: | ||
| """Return the complete parameter tuple of a registered instance. | ||
|
|
||
| The runtime type must be an exact registered implementation; instances | ||
| of unregistered subclasses are not recognized. | ||
|
|
||
| :raises TypeError: If the instance's runtime type is not registered. | ||
| """ | ||
| try: | ||
| return self._parameters_by_type[type(instance)] | ||
| except KeyError: | ||
| raise TypeError(f"Cannot infer {self.__name__} template parameters from {type(instance)!r}") from None | ||
|
|
||
| @property | ||
| def canonical_name(self) -> str: | ||
| """Fully qualified public name of this template family.""" | ||
| return self._canonical_name | ||
|
|
||
| @property | ||
| def metadata(self) -> TemplateMetadata: | ||
| """Return an immutable, current description for documentation tools.""" | ||
| instantiations = tuple( | ||
| TemplateInstantiation(parameters, implementation, f"{implementation.__module__}.{implementation.__name__}") | ||
| for parameters, implementation in self._instantiations.items() | ||
| ) | ||
| guide = None | ||
| if self._deduction_guide is not None: | ||
| guide = getattr(self._deduction_guide, "__qualname__", type(self._deduction_guide).__qualname__) | ||
| return TemplateMetadata(self.__name__, self._canonical_name, self._parameters, instantiations, guide) | ||
|
|
||
| @property | ||
| def instantiations(self) -> Mapping[tuple[object, ...], type]: | ||
| """Map registered parameter tuples to classes without allowing mutation.""" | ||
| return MappingProxyType(self._instantiations) | ||
|
|
||
| def is_instantiation(self, instance: object) -> bool: | ||
| """Return whether ``instance`` has a registered concrete runtime type.""" | ||
| return type(instance) in self._parameters_by_type | ||
|
|
||
| def __iter__(self) -> Iterator[tuple[object, ...]]: | ||
| """Iterate over registered parameter tuples in registration order.""" | ||
| return iter(self._instantiations) | ||
|
|
||
| def __repr__(self) -> str: | ||
| """Return a concise template-family representation.""" | ||
| return f"<template class {self._canonical_name}>" | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very nitpicky. I like the generic way, but it is a bit annoying that we have to write stormpy.ModelType.DTMC rather than just DTMC or stormpy.DTMC.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could be solved by aliasing these in stormpy:
Not sure if this is not more confusing? :D