diff --git a/doc/ref_kernel.rst b/doc/ref_kernel.rst index 896388d2911a6d3c0e7783d7b1b3833b87c770d0..46063310917fe4c111bac96a5a15c7c588974617 100644 --- a/doc/ref_kernel.rst +++ b/doc/ref_kernel.rst @@ -553,6 +553,8 @@ Helper values .. autoclass:: UniqueName +.. autoclass:: Optional + .. }}} Libraries: Extending and Interfacing with External Functionality diff --git a/doc/tutorial.rst b/doc/tutorial.rst index 397f34a987ed336795d00e2770c2fbeadf089ae7..3c85060dacf03b52f6e0b1faf05ad4697b6a5d07 100644 --- a/doc/tutorial.rst +++ b/doc/tutorial.rst @@ -1869,7 +1869,7 @@ Now to make things more interesting, we'll create a kernel with barriers: ... e[i,j,k] = c[i,j,k+1]+c[i,j,k-1] ... """ ... ], [ - ... lp.TemporaryVariable("c", lp.auto, shape=(50, 10, 99)), + ... lp.TemporaryVariable("c", dtype=None, shape=(50, 10, 99)), ... "..." ... ]) >>> knl = lp.add_and_infer_dtypes(knl, dict(a=np.int32)) diff --git a/loopy/__init__.py b/loopy/__init__.py index f50ce237c0170d2b9703f1760a334e9f39e7f7f1..d69a57bf1a5435adfb067df5cfb2080633cac765 100644 --- a/loopy/__init__.py +++ b/loopy/__init__.py @@ -149,6 +149,8 @@ from loopy.target.pyopencl import PyOpenCLTarget from loopy.target.ispc import ISPCTarget from loopy.target.numba import NumbaTarget, NumbaCudaTarget +from loopy.tools import Optional + __all__ = [ "TaggedVariable", "Reduction", "LinearSubscript", "TypeCast", @@ -275,6 +277,8 @@ __all__ = [ "NumbaTarget", "NumbaCudaTarget", "ASTBuilderBase", + "Optional", + # {{{ from this file "register_preamble_generators", diff --git a/loopy/kernel/array.py b/loopy/kernel/array.py index bae9d7d1fbc873076a84b933e5c78f5c9b19dbb5..3588f38af13479b127208c25735f1046eaa82706 100644 --- a/loopy/kernel/array.py +++ b/loopy/kernel/array.py @@ -693,8 +693,9 @@ class ArrayBase(ImmutableRecord): if dtype is lp.auto: from warnings import warn - warn("Argument/temporary data type should be None if unspecified, " - "not auto. This usage will be disallowed in 2018.", + warn("Argument/temporary data type for '%s' should be None if " + "unspecified, not auto. This usage will be disallowed in 2018." + % name, DeprecationWarning, stacklevel=2) dtype = None diff --git a/loopy/kernel/creation.py b/loopy/kernel/creation.py index c42db348234345a48efcb22b842fd114c5f65f14..2175b5f364796fed58d2373d82ae18ea7128ef2f 100644 --- a/loopy/kernel/creation.py +++ b/loopy/kernel/creation.py @@ -28,7 +28,7 @@ THE SOFTWARE. import numpy as np from pymbolic.mapper import CSECachingMapperMixin -from loopy.tools import intern_frozenset_of_ids +from loopy.tools import intern_frozenset_of_ids, Optional from loopy.symbolic import IdentityMapper, WalkMapper from loopy.kernel.data import ( InstructionBase, @@ -454,7 +454,6 @@ def parse_insn(groups, insn_options): and *inames_to_dup* is None or a list of tuples `(old, new)`. """ - import loopy as lp from loopy.symbolic import parse if "lhs" in groups: @@ -486,14 +485,11 @@ def parse_insn(groups, insn_options): for lhs_i in lhs: if isinstance(lhs_i, TypeAnnotation): - if lhs_i.type is None: - temp_var_types.append(lp.auto) - else: - temp_var_types.append(lhs_i.type) - + assert isinstance(lhs_i.type, Optional) + temp_var_types.append(lhs_i.type) lhs_i = lhs_i.child else: - temp_var_types.append(None) + temp_var_types.append(Optional()) inner_lhs_i = lhs_i if isinstance(inner_lhs_i, Lookup): @@ -1138,9 +1134,9 @@ class ArgumentGuesser: def make_new_arg(self, arg_name): arg_name = arg_name.strip() + import loopy as lp from loopy.kernel.data import ValueArg, ArrayArg, AddressSpace - import loopy as lp if arg_name in self.all_params: return ValueArg(arg_name) @@ -1191,7 +1187,7 @@ class ArgumentGuesser: for assignee_var_name, temp_var_type in zip( insn.assignee_var_names(), insn.temp_var_types): - if temp_var_type is not None: + if temp_var_type.has_value: temp_var_names.add(assignee_var_name) # }}} @@ -1431,7 +1427,7 @@ def create_temporaries(knl, default_order): insn.assignee_var_names(), insn.temp_var_types): - if temp_var_type is None: + if not temp_var_type.has_value: continue if assignee_name in new_temp_vars: @@ -1446,17 +1442,17 @@ def create_temporaries(knl, default_order): new_temp_vars[assignee_name] = lp.TemporaryVariable( name=assignee_name, - dtype=temp_var_type, + dtype=temp_var_type.value, address_space=lp.auto, base_indices=lp.auto, shape=lp.auto, order=default_order, target=knl.target) - if isinstance(insn, Assignment): - insn = insn.copy(temp_var_type=None) - else: - insn = insn.copy(temp_var_types=None) + if isinstance(insn, Assignment): + insn = insn.copy(temp_var_type=Optional()) + else: + insn = insn.copy(temp_var_types=(Optional(),) * len(insn.assignees)) new_insns.append(insn) diff --git a/loopy/kernel/data.py b/loopy/kernel/data.py index 7877f8b939444bf3dc095037ffeaa1bb548c39d6..8103029dc5f86896b4e08dd9e277b81b01e9ca27 100644 --- a/loopy/kernel/data.py +++ b/loopy/kernel/data.py @@ -330,8 +330,9 @@ class KernelArgument(ImmutableRecord): import loopy as lp if dtype is lp.auto: - warn("Argument/temporary data type should be None if unspecified, " - "not auto. This usage will be disallowed in 2018.", + warn("Argument/temporary data type for '%s' should be None if " + "unspecified, not auto. This usage will be disallowed in 2018." + % kwargs["name"], DeprecationWarning, stacklevel=2) dtype = None diff --git a/loopy/kernel/instruction.py b/loopy/kernel/instruction.py index e9c7bde9fd937dd58b5fa44476280a4b0c793dec..bfc06cff54c746277c3fb3fe47f35d7df8f54fcc 100644 --- a/loopy/kernel/instruction.py +++ b/loopy/kernel/instruction.py @@ -25,6 +25,7 @@ THE SOFTWARE. from six.moves import intern from pytools import ImmutableRecord, memoize_method from loopy.diagnostic import LoopyError +from loopy.tools import Optional from warnings import warn @@ -838,8 +839,9 @@ class Assignment(MultiAssignmentBase): .. attribute:: temp_var_type - if not *None*, a type that will be assigned to the new temporary variable - created from the assignee + A :class:`loopy.Optional`. If not empty, contains the type that + will be assigned to the new temporary variable created from the + assignment. .. attribute:: atomicity @@ -893,7 +895,7 @@ class Assignment(MultiAssignmentBase): within_inames_is_final=None, within_inames=None, boostable=None, boostable_into=None, tags=None, - temp_var_type=None, atomicity=(), + temp_var_type=Optional(), atomicity=(), priority=0, predicates=frozenset(), insn_deps=None, insn_deps_is_final=None, forced_iname_deps=None, forced_iname_deps_is_final=None): @@ -930,7 +932,8 @@ class Assignment(MultiAssignmentBase): self.assignee = assignee self.expression = expression - self.temp_var_type = temp_var_type + + self.temp_var_type = _check_and_fix_temp_var_type(temp_var_type) self.atomicity = atomicity # {{{ implement InstructionBase interface @@ -1006,8 +1009,9 @@ class CallInstruction(MultiAssignmentBase): .. attribute:: temp_var_types - if not *None*, a type that will be assigned to the new temporary variable - created from the assignee + A tuple of `:class:loopy.Optional`. If an entry is not empty, it + contains the type that will be assigned to the new temporary variable + created from the assigment. .. automethod:: __init__ """ @@ -1079,9 +1083,10 @@ class CallInstruction(MultiAssignmentBase): self.expression = expression if temp_var_types is None: - self.temp_var_types = (None,) * len(self.assignees) + self.temp_var_types = (Optional(),) * len(self.assignees) else: - self.temp_var_types = temp_var_types + self.temp_var_types = tuple( + _check_and_fix_temp_var_type(tvt) for tvt in temp_var_types) # {{{ implement InstructionBase interface @@ -1127,34 +1132,33 @@ class CallInstruction(MultiAssignmentBase): def make_assignment(assignees, expression, temp_var_types=None, **kwargs): - if len(assignees) > 1 or len(assignees) == 0: - atomicity = kwargs.pop("atomicity", ()) - if atomicity: - raise LoopyError("atomic operations with more than one " - "left-hand side not supported") + if temp_var_types is None: + temp_var_types = (Optional(),) * len(assignees) - from pymbolic.primitives import Call - from loopy.symbolic import Reduction - if not isinstance(expression, (Call, Reduction)): - raise LoopyError("right-hand side in multiple assignment must be " - "function call or reduction, got: '%s'" % expression) - - return CallInstruction( - assignees=assignees, - expression=expression, - temp_var_types=temp_var_types, - **kwargs) - - else: + if len(assignees) == 1: return Assignment( assignee=assignees[0], expression=expression, - temp_var_type=( - temp_var_types[0] - if temp_var_types is not None - else None), + temp_var_type=temp_var_types[0], **kwargs) + atomicity = kwargs.pop("atomicity", ()) + if atomicity: + raise LoopyError("atomic operations with more than one " + "left-hand side not supported") + + from pymbolic.primitives import Call + from loopy.symbolic import Reduction + if not isinstance(expression, (Call, Reduction)): + raise LoopyError("right-hand side in multiple assignment must be " + "function call or reduction, got: '%s'" % expression) + + return CallInstruction( + assignees=assignees, + expression=expression, + temp_var_types=temp_var_types, + **kwargs) + # {{{ c instruction @@ -1491,4 +1495,28 @@ def _get_insn_hash_key(insn): # }}} +# {{{ _check_and_fix_temp_var_type + +def _check_and_fix_temp_var_type(temp_var_type): + """Check temp_var_type for deprecated usage, and convert to the right value. + """ + + if temp_var_type is not None: + import loopy as lp + if temp_var_type is lp.auto: + warn("temp_var_type should be Optional(None) if " + "unspecified, not auto. This usage will be disallowed soon.", + DeprecationWarning, stacklevel=3) + temp_var_type = lp.Optional(None) + elif not isinstance(temp_var_type, lp.Optional): + warn("temp_var_type should be None or an instance of Optional. " + "Other values for temp_var_type will be disallowed soon.", + DeprecationWarning, stacklevel=3) + temp_var_type = lp.Optional(temp_var_type) + + return temp_var_type + +# }}} + + # vim: foldmethod=marker diff --git a/loopy/symbolic.py b/loopy/symbolic.py index f4d46854b8dd15c8c1e9a716017ce2724b4db2fc..ae0c8999eef2b4cfea2ff89eee28a85173916628 100644 --- a/loopy/symbolic.py +++ b/loopy/symbolic.py @@ -1131,14 +1131,15 @@ class LoopyParser(ParserBase): def parse_prefix(self, pstate): from pymbolic.parser import _PREC_UNARY, _less, _greater, _identifier + import loopy as lp if pstate.is_next(_less): pstate.advance() if pstate.is_next(_greater): - typename = None + typename = lp.Optional(None) pstate.advance() else: pstate.expect(_identifier) - typename = pstate.next_str() + typename = lp.Optional(pstate.next_str()) pstate.advance() pstate.expect(_greater) pstate.advance() diff --git a/loopy/tools.py b/loopy/tools.py index 7e9a8921477403a280f08c6c79fe380d5cb5b2f3..470921e31293192b6e96e90b02ec41968bc5fad9 100644 --- a/loopy/tools.py +++ b/loopy/tools.py @@ -587,6 +587,78 @@ class LazilyUnpicklingListWithEqAndPersistentHashing(LazilyUnpicklingList): # }}} +# {{{ optional object + +class _no_value(object): # noqa + pass + + +class Optional(object): + """A wrapper for an optionally present object. + + .. attribute:: has_value + + *True* if and only if this object contains a value. + + .. attribute:: value + + The value, if present. + """ + + __slots__ = ("has_value", "_value") + + def __init__(self, value=_no_value): + self.has_value = value is not _no_value + if self.has_value: + self._value = value + + def __str__(self): + if not self.has_value: + return "Optional()" + return "Optional(%s)" % self._value + + def __repr__(self): + if not self.has_value: + return "Optional()" + return "Optional(%r)" % self._value + + def __getstate__(self): + if not self.has_value: + return _no_value + + return (self._value,) + + def __setstate__(self, state): + if state is _no_value: + self.has_value = False + return + + self.has_value = True + self._value, = state + + def __eq__(self, other): + if not self.has_value: + return not other.has_value + + return self.value == other.value if other.has_value else False + + def __neq__(self, other): + return not self.__eq__(other) + + @property + def value(self): + if not self.has_value: + raise AttributeError("optional value not present") + return self._value + + def update_persistent_hash(self, key_hash, key_builder): + key_builder.rec( + key_hash, + (self._value,) if self.has_value else ()) + +# }}} + + def unpickles_equally(obj): from six.moves.cPickle import loads, dumps return loads(dumps(obj)) == obj diff --git a/test/test_misc.py b/test/test_misc.py index 05df0317a6a39823dc8ac0c6a51d992336bb81d1..7a834a6f5d393298e97df22d47a1de3b64354a42 100644 --- a/test/test_misc.py +++ b/test/test_misc.py @@ -287,6 +287,46 @@ def test_LazilyUnpicklingListWithEqAndPersistentHashing(): # }}} +def test_Optional(): # noqa + from loopy import Optional + + # {{{ test API + + opt = Optional() + assert not opt.has_value + with pytest.raises(AttributeError): + opt.value + + opt = Optional(1) + assert opt.has_value + assert 1 == opt.value + + assert Optional(1) == Optional(1) + assert Optional(1) != Optional(2) + assert Optional() == Optional() + assert Optional() != Optional(1) + + # }}} + + # {{{ test pickling + + import pickle + + assert not pickle.loads(pickle.dumps(Optional())).has_value + assert pickle.loads(pickle.dumps(Optional(1))).value == 1 + + # }}} + + # {{{ test key builder + + from loopy.tools import LoopyKeyBuilder + kb = LoopyKeyBuilder() + kb(Optional()) + kb(Optional(None)) + + # }}} + + if __name__ == "__main__": if len(sys.argv) > 1: exec(sys.argv[1])