blob: 99987b77f98d1bef142e2f1e6a5163b9283e725a [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Cross-language enum singletons."""
from __future__ import annotations
import typing
from collections.abc import Callable, Iterator
from typing import TYPE_CHECKING, Any, ClassVar, TypeVar, overload
from typing_extensions import Self, get_annotations
from .. import core
from ..container import Dict, List
from ..core import Object
from .c_class import c_class
from .py_class import py_class
if TYPE_CHECKING:
from enum import EnumMeta as _EnumMetaBase
else:
_EnumMetaBase = type(Object)
if TYPE_CHECKING:
_EnumClsT = TypeVar("_EnumClsT", bound=type)
def _enum_c_class(type_key: str, **kwargs: Any) -> Callable[[_EnumClsT], _EnumClsT]:
return lambda cls: cls
else:
_enum_c_class = c_class
__all__ = [
"ENUM_STATE_ATTR",
"Enum",
"EnumAttrMap",
"EnumState",
"IntEnum",
"StrEnum",
"auto",
"entry",
]
ENUM_STATE_ATTR = "__ffi_enum__"
_INT64_MIN = -(1 << 63)
_INT64_MAX = (1 << 63) - 1
@c_class("ffi.EnumState")
class EnumState(Object):
"""Registry state shared by enum definition and lookup."""
entries: List[Object]
indexes: Dict[Any, Object]
attrs: Dict[str, Dict[Object, Any]]
if TYPE_CHECKING:
def __init__(self) -> None: ...
def __ffi_init__(self) -> None: ... # ty: ignore[invalid-method-override]
class _EnumEntry:
__slots__ = ("args", "kwargs")
def __init__(self, *args: Any, **kwargs: Any) -> None:
self.args = args
self.kwargs = kwargs
def __repr__(self) -> str:
values = [repr(arg) for arg in self.args]
values.extend(f"{key}={value!r}" for key, value in self.kwargs.items())
return f"entry({', '.join(values)})"
def entry(*args: Any, **kwargs: Any) -> Any:
"""Declare an enum variant, optionally with declared-field values."""
return _EnumEntry(*args, **kwargs)
def auto() -> Any:
"""Declare a variant whose indices are inferred from its class-body alias."""
return _EnumEntry()
class _ClassProperty:
__slots__ = ("fget",)
def __init__(self, fget: Callable[[type], Any]) -> None:
self.fget = fget
def __get__(self, instance: Any, owner: type | None = None) -> Any:
return self.fget(owner if owner is not None else type(instance))
def _normalize_index(index: Any) -> int | str:
if isinstance(index, bool):
return int(index)
if isinstance(index, int):
if not _INT64_MIN <= index <= _INT64_MAX:
raise OverflowError(f"enum integer index {index} is outside int64")
return index
if isinstance(index, str):
return index
raise TypeError(f"enum index must be int or str, got {type(index).__name__}")
def _state(cls: type, *, create: bool = False) -> EnumState | None:
type_info = getattr(cls, "__tvm_ffi_type_info__", None)
if type_info is None:
return None
state = core._lookup_type_attr(type_info.type_index, ENUM_STATE_ATTR)
if state is None and create:
state = EnumState()
core._register_type_attr(type_info.type_index, ENUM_STATE_ATTR, state)
state = core._lookup_type_attr(type_info.type_index, ENUM_STATE_ATTR)
return state
def _ordered_entries(cls: type) -> list[Any]:
state = _state(cls)
return [] if state is None else list(state.entries)
class _EnumMeta(_EnumMetaBase):
def __iter__(cls) -> Iterator[Any]:
return iter(_ordered_entries(cls))
def __len__(cls) -> int:
return len(_ordered_entries(cls))
def __call__(cls, *args: Any, **kwargs: Any) -> Any:
if not kwargs and len(args) == 1:
value = args[0]
if isinstance(value, cls):
return value
if isinstance(value, (int, str)):
try:
return cls.get(value) # ty: ignore[unresolved-attribute]
except KeyError:
raise ValueError(f"{value!r} is not a valid {cls.__name__}") from None
return super().__call__(*args, **kwargs)
@_enum_c_class("ffi.Enum", init=False)
class Enum(Object, metaclass=_EnumMeta):
"""Base class for FFI-registered enum singletons."""
if not TYPE_CHECKING:
__slots__ = ()
_int_index: int
_str_index: str
if TYPE_CHECKING:
@property
def _int_index(self) -> int: ...
@property
def _str_index(self) -> str: ...
@property
def name(self) -> str:
"""Return the canonical string index."""
...
@property
def value(self) -> Any:
"""Return the payload index exposed by a payload enum."""
...
@overload
def __new__(cls: type[Self], index: Self, /) -> Self: ...
@overload
def __new__(cls: type[Self], index: int | str, /) -> Self: ...
@overload
def __new__(cls: type[Self], *args: Any, **kwargs: Any) -> Self: ...
def __new__(cls: type[Self], *args: Any, **kwargs: Any) -> Self:
"""Normalize a canonical index to its singleton."""
...
def __init_subclass__(
cls,
*,
type_key: str | None = None,
frozen: bool = True,
init: bool = True,
repr: bool = True,
**kwargs: Any,
) -> None:
super().__init_subclass__(**kwargs)
if type_key is None:
return
payload_type = (
int if issubclass(cls, IntEnum) else str if issubclass(cls, StrEnum) else None
)
user_repr = "__repr__" in cls.__dict__
binders, declarations = _collect_declarations(cls, payload_type)
cxx_backed = core._object_type_key_to_index(type_key) is not None
if cxx_backed:
c_class(type_key, init=init, repr=repr)(cls)
else:
py_class(type_key, frozen=frozen, structural_eq="singleton")(cls)
if payload_type is not None:
_install_payload_behavior(cls, payload_type, user_repr)
_resolve(cls, binders, declarations, type_key, cxx_backed, payload_type)
@classmethod
def get(cls, index: int | str) -> Enum:
"""Return the singleton at a canonical integer or string index."""
index = _normalize_index(index)
state = _state(cls)
if state is not None and index in state.indexes:
return typing.cast(Enum, state.indexes[index])
raise KeyError(index)
@classmethod
def all_entries(cls) -> Iterator[Enum]:
"""Iterate over variants in registration order."""
return iter(_ordered_entries(cls))
@_ClassProperty
def attr_dict(cls: type) -> Any:
"""Return the live extensible-attribute dictionary."""
state = _state(cls)
return Dict({}) if state is None else state.attrs
@classmethod
def def_attr(cls, name: str, *, default: Any = core.MISSING) -> EnumAttrMap:
"""Return a per-variant extensible-attribute view."""
return EnumAttrMap(cls, name, default=default)
class EnumAttrMap:
"""Mutable singleton-keyed view of one extensible enum attribute."""
__slots__ = ("_default", "_enum_cls", "_name")
def __init__(self, enum_cls: type, name: str, *, default: Any = core.MISSING) -> None:
self._enum_cls = enum_cls
self._name = name
self._default = default
def _check(self, variant: object) -> None:
if not isinstance(variant, self._enum_cls):
raise TypeError(f"expected {self._enum_cls.__name__}, got {type(variant).__name__}")
def _column(self, create: bool) -> Any:
state = _state(self._enum_cls, create=create)
if state is None:
return None
attrs = state.attrs
column = attrs.get(self._name)
if column is None and create:
column = Dict({})
attrs[self._name] = column
return column
def __setitem__(self, variant: object, value: Any) -> None:
self._check(variant)
self._column(True)[variant] = value
def __getitem__(self, variant: object) -> Any:
self._check(variant)
column = self._column(False)
if column is not None and variant in column:
return column[variant]
if self._default is not core.MISSING:
return self._default
raise KeyError(f"{variant!r} has no extensible attribute {self._name!r}")
def __contains__(self, variant: object) -> bool:
if not isinstance(variant, self._enum_cls):
return False
column = self._column(False)
return column is not None and variant in column
def get(self, variant: object, default: Any = None) -> Any:
"""Return a stored value or *default*."""
if variant not in self:
return default
return self[variant]
@property
def name(self) -> str:
"""Return the extensible-attribute name."""
return self._name
def _install_payload_behavior(cls: type, payload_type: type, user_repr: bool) -> None:
def eq(self: Enum, other: object) -> Any:
if isinstance(other, type(self)):
return self.value == other.value
if isinstance(other, payload_type):
return self.value == other
return NotImplemented
def ne(self: Enum, other: object) -> Any:
result = eq(self, other)
return NotImplemented if result is NotImplemented else not result
def name(self: Enum) -> str:
return str(self._str_index)
defaults = {
"__eq__": eq,
"__ne__": ne,
"__str__": lambda self: str(self.value),
"__hash__": lambda self: hash(self.value),
}
for key, value in defaults.items():
if key not in cls.__dict__:
setattr(cls, key, value)
if "name" not in cls.__dict__:
cls.name = property(name) # ty: ignore[unresolved-attribute]
if not user_repr:
cls.__repr__ = lambda self: f"{type(self).__name__}.{self.name}" # type: ignore[attr-defined]
@_enum_c_class("ffi.IntEnum", init=False)
class IntEnum(Enum):
"""Enum whose ``value`` aliases its signed 64-bit integer index."""
if not TYPE_CHECKING:
__slots__ = ()
value: int
if TYPE_CHECKING:
@property
def value(self) -> int:
"""Return the canonical integer index."""
...
def __init_subclass__(cls, **kwargs: Any) -> None:
_reject_value_field(cls, "IntEnum")
super().__init_subclass__(**kwargs)
@_enum_c_class("ffi.StrEnum", init=False)
class StrEnum(Enum):
"""Enum whose ``name`` and ``value`` alias its string index."""
if not TYPE_CHECKING:
__slots__ = ()
value: str
if TYPE_CHECKING:
@property
def value(self) -> str:
"""Return the canonical string index."""
...
def __init_subclass__(cls, **kwargs: Any) -> None:
_reject_value_field(cls, "StrEnum")
super().__init_subclass__(**kwargs)
def _collect_declarations(
cls: type, payload_type: type | None
) -> tuple[list[str], dict[str, _EnumEntry]]:
annotations = _own_annotations(cls)
binders = [
name
for name, annotation in annotations.items()
if not name.startswith("_") and _is_class_var(annotation) and name not in cls.__dict__
]
fields = {name for name, annotation in annotations.items() if not _is_class_var(annotation)}
declarations: dict[str, _EnumEntry] = {}
for name, value in list(cls.__dict__.items()):
if name.startswith("_") or name in fields:
continue
if isinstance(value, _EnumEntry):
declarations[name] = value
elif payload_type is not None and not (
isinstance(value, (staticmethod, classmethod, property)) or callable(value)
):
declarations[name] = _EnumEntry(value=value)
else:
continue
delattr(cls, name)
return binders, declarations
def _resolve(
cls: type,
binders: list[str],
declarations: dict[str, _EnumEntry],
type_key: str,
cxx_backed: bool,
payload_type: type | None,
) -> None:
state = _state(cls, create=True)
assert state is not None
entries, indexes = state.entries, state.indexes
for alias in binders:
instance = indexes.get(alias)
if instance is None:
if cxx_backed:
raise RuntimeError(f"{type_key!r} has no enum at string index {alias!r}")
if payload_type is int:
raise TypeError(f"{cls.__name__}.{alias} requires an integer value")
instance = _create(cls, len(entries), alias, _EnumEntry(), cxx_backed)
_register(entries, indexes, instance)
setattr(cls, alias, instance)
for alias, declaration in declarations.items():
int_index, str_index, fields = _indices(cls, alias, len(entries), declaration, payload_type)
lookup = int_index if payload_type is int else str_index
instance = indexes.get(lookup)
if instance is None:
if int_index in indexes or str_index in indexes:
raise ValueError(f"duplicate enum index in {cls.__name__}.{alias}")
instance = _create(cls, int_index, str_index, fields, cxx_backed)
_register(entries, indexes, instance)
setattr(cls, alias, instance)
def _indices(
cls: type,
alias: str,
ordinal: int,
declaration: _EnumEntry,
payload_type: type | None,
) -> tuple[int, str, _EnumEntry]:
kwargs = dict(declaration.kwargs)
if "_int_index" in kwargs or "_str_index" in kwargs:
raise TypeError(f"{cls.__name__}.{alias}: enum indices are inferred")
if payload_type is None:
return ordinal, alias, declaration
if "value" not in kwargs:
raise TypeError(f"{cls.__name__}.{alias} requires a {payload_type.__name__} value")
value = kwargs.pop("value")
if type(value) is not payload_type:
raise TypeError(f"{cls.__name__}.{alias} value must be {payload_type.__name__}")
if payload_type is int:
_normalize_index(value)
indices = value, alias
else:
indices = ordinal, value
return indices[0], indices[1], _EnumEntry(*declaration.args, **kwargs)
def _create(
cls: type, int_index: int, str_index: str, declaration: _EnumEntry, cxx_backed: bool
) -> Any:
try:
if not cxx_backed:
return cls(
*declaration.args,
_int_index=int_index,
_str_index=str_index,
**declaration.kwargs,
)
if declaration.args:
raise TypeError("C++-backed enum entries require keyword fields")
type_info = cls.__tvm_ffi_type_info__ # ty: ignore[unresolved-attribute]
ffi_new = core._lookup_type_attr(type_info.type_index, "__ffi_new__")
if ffi_new is None:
raise TypeError("enum type has no allocator")
instance = ffi_new()
values = {"_int_index": int_index, "_str_index": str_index, **declaration.kwargs}
for name, value in values.items():
descriptor = getattr(cls, name, None)
if descriptor is None or not hasattr(descriptor, "set"):
raise TypeError(f"field {name!r} has no reflected setter")
descriptor.set(instance, value)
return instance
except Exception as err:
raise TypeError(f"{cls.__name__}.{str_index}: invalid enum entry: {err}") from None
def _register(entries: Any, indexes: Any, instance: Any) -> None:
entries.append(instance)
indexes[instance._int_index] = instance
indexes[instance._str_index] = instance
def _own_annotations(cls: type) -> dict[str, Any]:
return get_annotations(cls)
def _is_class_var(annotation: Any) -> bool:
if annotation is ClassVar or typing.get_origin(annotation) is ClassVar:
return True
if isinstance(annotation, str):
return annotation.replace(" ", "").startswith(("ClassVar", "typing.ClassVar"))
return False
def _reject_value_field(cls: type, base_name: str) -> None:
if "value" in cls.__dict__ or "value" in _own_annotations(cls):
raise TypeError(f"{base_name} reserves `value` as its canonical index alias")