""" This module contains utils for manipulating target configurations such as compiler flags. """ import re import zlib import base64 from types import MappingProxyType from numba.core import utils class Option: """An option to be used in ``TargetConfig``. """ __slots__ = "_type", "_default", "_doc" def __init__(self, type, *, default, doc): """ Parameters ---------- type : Type of the option value. It can be a callable. The setter always calls ``self._type(value)``. default : The default value for the option. doc : str Docstring for the option. """ self._type = type self._default = default self._doc = doc @property def type(self): return self._type @property def default(self): return self._default @property def doc(self): return self._doc class _FlagsStack(utils.ThreadLocalStack, stack_name="flags"): pass class ConfigStack: """A stack for tracking target configurations in the compiler. It stores the stack in a thread-local class attribute. All instances in the same thread will see the same stack. """ @classmethod def top_or_none(cls): """Get the TOS or return None if no config is set. """ self = cls() if self: flags = self.top() else: # Note: should this be the default flag for the target instead? flags = None return flags def __init__(self): self._stk = _FlagsStack() def top(self): return self._stk.top() def __len__(self): return len(self._stk) def enter(self, flags): """Returns a contextmanager that performs ``push(flags)`` on enter and ``pop()`` on exit. """ return self._stk.enter(flags) class _MetaTargetConfig(type): """Metaclass for ``TargetConfig``. When a subclass of ``TargetConfig`` is created, all ``Option`` defined as class members will be parsed and corresponding getters, setters, and delters will be inserted. """ def __init__(cls, name, bases, dct): """Invoked when subclass is created. Insert properties for each ``Option`` that are class members. All the options will be grouped inside the ``.options`` class attribute. """ # Gather options from base classes and class dict opts = {} # Reversed scan into the base classes to follow MRO ordering such that # the closest base class is overriding for base_cls in reversed(bases): opts.update(base_cls.options) opts.update(cls.find_options(dct)) # Store the options into class attribute as a ready-only mapping. cls.options = MappingProxyType(opts) # Make properties for each of the options def make_prop(name, option): def getter(self): return self._values.get(name, option.default) def setter(self, val): self._values[name] = option.type(val) def delter(self): del self._values[name] return property(getter, setter, delter, option.doc) for name, option in cls.options.items(): setattr(cls, name, make_prop(name, option)) def find_options(cls, dct): """Returns a new dict with all the items that are a mapping to an ``Option``. """ return {k: v for k, v in dct.items() if isinstance(v, Option)} class _NotSetType: def __repr__(self): return "" _NotSet = _NotSetType() class TargetConfig(metaclass=_MetaTargetConfig): """Base class for ``TargetConfig``. Subclass should fill class members with ``Option``. For example: >>> class MyTargetConfig(TargetConfig): >>> a_bool_option = Option(type=bool, default=False, doc="a bool") >>> an_int_option = Option(type=int, default=0, doc="an int") The metaclass will insert properties for each ``Option``. For exapmle: >>> tc = MyTargetConfig() >>> tc.a_bool_option = True # invokes the setter >>> print(tc.an_int_option) # print the default """ # Used for compression in mangling. # Set to -15 to disable the header and checksum for smallest output. _ZLIB_CONFIG = {"wbits": -15} def __init__(self, copy_from=None): """ Parameters ---------- copy_from : TargetConfig or None if None, creates an empty ``TargetConfig``. Otherwise, creates a copy. """ self._values = {} if copy_from is not None: assert isinstance(copy_from, TargetConfig) self._values.update(copy_from._values) def __repr__(self): # NOTE: default options will be placed at the end and grouped inside # a square bracket; i.e. [optname=optval, ...] args = [] defs = [] for k in self.options: msg = f"{k}={getattr(self, k)}" if not self.is_set(k): defs.append(msg) else: args.append(msg) clsname = self.__class__.__name__ return f"{clsname}({', '.join(args)}, [{', '.join(defs)}])" def __hash__(self): return hash(tuple(sorted(self.values()))) def __eq__(self, other): if isinstance(other, TargetConfig): return self.values() == other.values() else: return NotImplemented def values(self): """Returns a dict of all the values """ return {k: getattr(self, k) for k in self.options} def is_set(self, name): """Is the option set? """ self._guard_option(name) return name in self._values def discard(self, name): """Remove the option by name if it is defined. After this, the value for the option will be set to its default value. """ self._guard_option(name) self._values.pop(name, None) def inherit_if_not_set(self, name, default=_NotSet): """Inherit flag from ``ConfigStack``. Parameters ---------- name : str Option name. default : optional When given, it overrides the default value. It is only used when the flag is not defined locally and there is no entry in the ``ConfigStack``. """ self._guard_option(name) if not self.is_set(name): cstk = ConfigStack() if cstk: # inherit top = cstk.top() setattr(self, name, getattr(top, name)) elif default is not _NotSet: setattr(self, name, default) def copy(self): """Clone this instance. """ return type(self)(self) def summary(self) -> str: """Returns a ``str`` that summarizes this instance. In contrast to ``__repr__``, only options that are explicitly set will be shown. """ args = [f"{k}={v}" for k, v in self._summary_args()] clsname = self.__class__.__name__ return f"{clsname}({', '.join(args)})" def _guard_option(self, name): if name not in self.options: msg = f"{name!r} is not a valid option for {type(self)}" raise ValueError(msg) def _summary_args(self): """returns a sorted sequence of 2-tuple containing the ``(flag_name, flag_value)`` for flag that are set with a non-default value. """ args = [] for k in sorted(self.options): opt = self.options[k] if self.is_set(k): flagval = getattr(self, k) if opt.default != flagval: v = (k, flagval) args.append(v) return args @classmethod def _make_compression_dictionary(cls) -> bytes: """Returns a ``bytes`` object suitable for use as a dictionary for compression. """ buf = [] # include package name buf.append("numba") # include class name buf.append(cls.__class__.__name__) # include common values buf.extend(["True", "False"]) # include all options name and their default value for k, opt in cls.options.items(): buf.append(k) buf.append(str(opt.default)) return ''.join(buf).encode() def get_mangle_string(self) -> str: """Return a string suitable for symbol mangling. """ zdict = self._make_compression_dictionary() comp = zlib.compressobj(zdict=zdict, level=zlib.Z_BEST_COMPRESSION, **self._ZLIB_CONFIG) # The mangled string is a compressed and base64 encoded version of the # summary buf = [comp.compress(self.summary().encode())] buf.append(comp.flush()) return base64.b64encode(b''.join(buf)).decode() @classmethod def demangle(cls, mangled: str) -> str: """Returns the demangled result from ``.get_mangle_string()`` """ # unescape _XX sequence def repl(x): return chr(int('0x' + x.group(0)[1:], 16)) unescaped = re.sub(r"_[a-zA-Z0-9][a-zA-Z0-9]", repl, mangled) # decode base64 raw = base64.b64decode(unescaped) # decompress zdict = cls._make_compression_dictionary() dc = zlib.decompressobj(zdict=zdict, **cls._ZLIB_CONFIG) buf = [] while raw: buf.append(dc.decompress(raw)) raw = dc.unconsumed_tail buf.append(dc.flush()) return b''.join(buf).decode()