From b5f1e16009ed38e3853696e947222345acff3692 Mon Sep 17 00:00:00 2001 From: Matt Wala Date: Sun, 18 Nov 2018 18:24:42 -0600 Subject: [PATCH 1/6] Don't use lp.auto in temp_var_type to represent no value (closes #152) Switching to *None* is not ideal, because *None* is already used in temp_var_type to represent no value. Hence, this change introduces an "optional" object wrapper to loopy and uses it to represent the absence of a temp_var_type in an assignment instruction. --- doc/ref_kernel.rst | 2 ++ loopy/__init__.py | 4 +++ loopy/kernel/creation.py | 26 ++++++-------- loopy/kernel/instruction.py | 58 +++++++++++++++--------------- loopy/symbolic.py | 5 +-- loopy/tools.py | 72 +++++++++++++++++++++++++++++++++++++ test/test_misc.py | 35 ++++++++++++++++++ 7 files changed, 157 insertions(+), 45 deletions(-) diff --git a/doc/ref_kernel.rst b/doc/ref_kernel.rst index c9ce20626..d3bbfb0ea 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/loopy/__init__.py b/loopy/__init__.py index f50ce237c..d69a57bf1 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/creation.py b/loopy/kernel/creation.py index c42db3482..3ae293667 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, @@ -486,14 +486,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): @@ -1140,7 +1137,6 @@ class ArgumentGuesser: arg_name = arg_name.strip() 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/instruction.py b/loopy/kernel/instruction.py index e9c7bde9f..d7784eabf 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): @@ -1006,8 +1008,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,7 +1082,7 @@ 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 @@ -1127,34 +1130,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 diff --git a/loopy/symbolic.py b/loopy/symbolic.py index f4d46854b..ae0c8999e 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 8c5d36390..93d83e0f3 100644 --- a/loopy/tools.py +++ b/loopy/tools.py @@ -582,6 +582,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 () + + return (self._value,) + + def __setstate__(self, state): + if not state: + 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 05df0317a..adbadd182 100644 --- a/test/test_misc.py +++ b/test/test_misc.py @@ -287,6 +287,41 @@ 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 + + # }}} + + # {{{ 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]) -- GitLab From 9c5d835db834e260386ed0440a6466401de7af5c Mon Sep 17 00:00:00 2001 From: Matt Wala Date: Sun, 18 Nov 2018 19:32:40 -0600 Subject: [PATCH 2/6] Fix a deprecated usage in tutorial. --- doc/tutorial.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/tutorial.rst b/doc/tutorial.rst index 1272d2a59..ce0677147 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)) -- GitLab From 624083bcc13e46a9f06138547ba846faa7b6d5e5 Mon Sep 17 00:00:00 2001 From: Matt Wala Date: Sun, 18 Nov 2018 19:35:33 -0600 Subject: [PATCH 3/6] Make warnings more informative --- loopy/kernel/array.py | 5 +++-- loopy/kernel/creation.py | 2 +- loopy/kernel/data.py | 5 +++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/loopy/kernel/array.py b/loopy/kernel/array.py index 6bf733a84..7130e815c 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 3ae293667..92ce79bb5 100644 --- a/loopy/kernel/creation.py +++ b/loopy/kernel/creation.py @@ -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: @@ -1145,6 +1144,7 @@ class ArgumentGuesser: # It's not a temp var, and thereby not a domain parameter--the only # other writable type of variable is an argument. + import loopy as lp return ArrayArg(arg_name, shape=lp.auto, offset=self.default_offset, diff --git a/loopy/kernel/data.py b/loopy/kernel/data.py index 3e776bd06..ddc4dd79b 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 -- GitLab From fca5d7f8939695d5763e54c2ac5fb5ccdf908940 Mon Sep 17 00:00:00 2001 From: Matt Wala Date: Sun, 18 Nov 2018 20:18:06 -0600 Subject: [PATCH 4/6] Fix import nesting --- loopy/kernel/creation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/loopy/kernel/creation.py b/loopy/kernel/creation.py index 92ce79bb5..2175b5f36 100644 --- a/loopy/kernel/creation.py +++ b/loopy/kernel/creation.py @@ -1134,6 +1134,7 @@ 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 @@ -1144,7 +1145,6 @@ class ArgumentGuesser: # It's not a temp var, and thereby not a domain parameter--the only # other writable type of variable is an argument. - import loopy as lp return ArrayArg(arg_name, shape=lp.auto, offset=self.default_offset, -- GitLab From 79601adec3fe822d0955a89bcb02123783873560 Mon Sep 17 00:00:00 2001 From: Matt Wala Date: Sun, 18 Nov 2018 20:50:14 -0600 Subject: [PATCH 5/6] Fix 2.7 pickling --- loopy/tools.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/loopy/tools.py b/loopy/tools.py index 93d83e0f3..443aaa166 100644 --- a/loopy/tools.py +++ b/loopy/tools.py @@ -619,12 +619,12 @@ class Optional(object): def __getstate__(self): if not self.has_value: - return () + return _no_value return (self._value,) def __setstate__(self, state): - if not state: + if state is _no_value: self.has_value = False return -- GitLab From 967bc16511ae578d212c3d220c7e2f2270be29a7 Mon Sep 17 00:00:00 2001 From: Matt Wala Date: Mon, 19 Nov 2018 16:59:47 -0600 Subject: [PATCH 6/6] Test equality --- test/test_misc.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/test_misc.py b/test/test_misc.py index adbadd182..7a834a6f5 100644 --- a/test/test_misc.py +++ b/test/test_misc.py @@ -301,6 +301,11 @@ def test_Optional(): # noqa 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 -- GitLab