From b6d6ef374bb1432955b1760dd3fd806ddcdec716 Mon Sep 17 00:00:00 2001
From: Andreas Kloeckner <inform@tiker.net>
Date: Tue, 6 Feb 2018 13:49:15 -0600
Subject: [PATCH] Add check for variable ordering and language versioning
 scheme

---
 doc/misc.rst                   |   4 +-
 doc/ref_creation.rst           |   2 +
 examples/python/hello-loopy.py |   3 +-
 loopy/__init__.py              |   4 ++
 loopy/check.py                 | 115 +++++++++++++++++++++++++++++++++
 loopy/kernel/creation.py       |  35 ++++++++++
 loopy/options.py               |  11 ++++
 loopy/version.py               |  52 +++++++++++++++
 test/test_dg.py                |   3 +-
 test/test_loopy.py             |  13 ++--
 10 files changed, 235 insertions(+), 7 deletions(-)

diff --git a/doc/misc.rst b/doc/misc.rst
index cd6fe102c..2c9c9a92b 100644
--- a/doc/misc.rst
+++ b/doc/misc.rst
@@ -90,7 +90,9 @@ regarding OpenCL drivers.
 User-visible Changes
 ====================
 
-Version 2017.2
+See also :ref:`language-versioning`.
+
+Version 2018.1
 --------------
 .. note::
 
diff --git a/doc/ref_creation.rst b/doc/ref_creation.rst
index 92eff09c9..6b715033c 100644
--- a/doc/ref_creation.rst
+++ b/doc/ref_creation.rst
@@ -30,4 +30,6 @@ To Copy between Data Formats
 
 .. autofunction:: make_copy_kernel
 
+.. automodule:: loopy.version
+
 .. vim: tw=75:spell:fdm=marker
diff --git a/examples/python/hello-loopy.py b/examples/python/hello-loopy.py
index 7c5de5a1b..e7ab13c16 100644
--- a/examples/python/hello-loopy.py
+++ b/examples/python/hello-loopy.py
@@ -15,7 +15,8 @@ a = cl.array.arange(queue, n, dtype=np.float32)
 # ------
 knl = lp.make_kernel(
         "{ [i]: 0<=i<n }",
-        "out[i] = 2*a[i]")
+        "out[i] = 2*a[i]",
+        lang_version=(2018, 1))
 
 # transform
 # ---------
diff --git a/loopy/__init__.py b/loopy/__init__.py
index 5e8a3fb06..0f4697f92 100644
--- a/loopy/__init__.py
+++ b/loopy/__init__.py
@@ -65,6 +65,8 @@ from loopy.library.reduction import register_reduction_parser
 
 # {{{ import transforms
 
+from loopy.version import VERSION, MOST_RECENT_LANGUAGE_VERSION
+
 from loopy.transform.iname import (
         set_loop_priority, prioritize_loops,
         split_iname, chunk_iname, join_inames, tag_inames, duplicate_inames,
@@ -171,6 +173,8 @@ __all__ = [
 
         "register_reduction_parser",
 
+        "VERSION", "MOST_RECENT_LANGUAGE_VERSION",
+
         # {{{ transforms
 
         "set_loop_priority", "prioritize_loops",
diff --git a/loopy/check.py b/loopy/check.py
index 7e661b566..b4e117e25 100644
--- a/loopy/check.py
+++ b/loopy/check.py
@@ -379,6 +379,120 @@ def check_has_schedulable_iname_nesting(kernel):
                 "to get hints about which iname to duplicate. Here are some "
                 "options:\n%s" % opt_str)
 
+
+class IndirectDependencyEdgeFinder(object):
+    def __init__(self, kernel):
+        self.kernel = kernel
+        self.dep_edge_cache = {}
+
+    def __call__(self, depender_id, dependee_id):
+        cache_key = (depender_id, dependee_id)
+
+        try:
+            return self.dep_edge_cache[cache_key]
+        except KeyError:
+            pass
+
+        depender = self.kernel.id_to_insn[depender_id]
+
+        if dependee_id in depender.depends_on:
+            self.dep_edge_cache[cache_key] = True
+            return True
+
+        for dep in depender.depends_on:
+            if self(dep, dependee_id):
+                self.dep_edge_cache[cache_key] = True
+                return True
+
+        return False
+
+
+def needs_no_sync_with(kernel, var_scope, dep_a_id, dep_b_id):
+    dep_a = kernel.id_to_insn[dep_a_id]
+    dep_b = kernel.id_to_insn[dep_b_id]
+
+    from loopy.kernel.data import temp_var_scope
+    if var_scope == temp_var_scope.GLOBAL:
+        search_scopes = ["global", "any"]
+    elif var_scope == temp_var_scope.LOCAL:
+        search_scopes = ["local", "any"]
+    elif var_scope == temp_var_scope.PRIVATE:
+        search_scopes = ["any"]
+    else:
+        raise ValueError("unexpected value of 'temp_var_scope'")
+
+    for scope in search_scopes:
+        if (dep_a_id, scope) in dep_b.no_sync_with:
+            return True
+        if (dep_b_id, scope) in dep_a.no_sync_with:
+            return True
+
+    return False
+
+
+def check_variable_access_ordered(kernel):
+    """Checks that all writes are ordered with respect to all other access to
+    the written variable.
+    """
+    checked_variables = (
+            kernel.get_written_variables()
+            | set(kernel.temporary_variables)
+            | set(arg for arg in kernel.arg_dict))
+
+    wmap = kernel.writer_map()
+    rmap = kernel.reader_map()
+
+    from loopy.kernel.data import GlobalArg, ValueArg, temp_var_scope
+
+    depfind = IndirectDependencyEdgeFinder(kernel)
+
+    for name in checked_variables:
+        if name in kernel.temporary_variables:
+            scope = kernel.temporary_variables[name].scope
+        else:
+            arg = kernel.arg_dict[name]
+            if isinstance(arg, GlobalArg):
+                scope = temp_var_scope.GLOBAL
+            elif isinstance(arg, ValueArg):
+                scope = temp_var_scope.PRIVATE
+            else:
+                raise ValueError("could not determine scope of '%s'" % name)
+
+        # Check even for PRIVATE scope, to ensure intentional program order.
+
+        readers = rmap.get(name, set())
+        writers = wmap.get(name, set())
+
+        for writer_id in writers:
+            for other_id in readers | writers:
+                if writer_id == other_id:
+                    continue
+
+                has_ordering_relationship = (
+                        needs_no_sync_with(kernel, scope, other_id, writer_id)
+                        or
+                        depfind(writer_id, other_id)
+                        or
+                        depfind(other_id, writer_id))
+
+                if not has_ordering_relationship:
+                    msg = ("No ordering relationship found between "
+                            "'{writer_id}' which writes '{var}' and "
+                            "'{other_id}' which also accesses '{var}'. "
+                            "Please either add a (possibly indirect) dependency "
+                            "between the two, or add one to the other's no_sync set "
+                            "to indicate that no ordering is intended."
+                            .format(
+                                writer_id=writer_id,
+                                other_id=other_id,
+                                var=name))
+                    if kernel.options.enforce_check_variable_access_ordered:
+                        raise LoopyError(msg)
+                    else:
+                        from loopy.diagnostic import warn_with_kernel
+                        warn_with_kernel(
+                                kernel, "variable_access_ordered", msg)
+
 # }}}
 
 
@@ -397,6 +511,7 @@ def pre_schedule_checks(kernel):
         check_bounds(kernel)
         check_write_destinations(kernel)
         check_has_schedulable_iname_nesting(kernel)
+        check_variable_access_ordered(kernel)
 
         logger.debug("%s: pre-schedule check: done" % kernel.name)
     except KeyboardInterrupt:
diff --git a/loopy/kernel/creation.py b/loopy/kernel/creation.py
index 4a08c28bd..15cb29c22 100644
--- a/loopy/kernel/creation.py
+++ b/loopy/kernel/creation.py
@@ -1909,6 +1909,23 @@ def make_kernel(domains, instructions, kernel_data=["..."], **kwargs):
         will be fixed to *value*. *name* may refer to :ref:`domain-parameters`
         or :ref:`arguments`. See also :func:`loopy.fix_parameters`.
 
+    :arg lang_version: The language version against which the kernel was
+        written, a tuple. To ensure future compatibility, copy the current value of
+        :data:`loopy.MOST_RECENT_LANGUAGE_VERSION` and pass that value.
+
+        (If you just pass :data:`loopy.MOST_RECENT_LANGUAGE_VERSION` directly,
+        breaking language changes *will* apply to your kernel without asking,
+        likely breaking your code.)
+
+        If not given, this value defaults to version **(2017, 2, 1)** and
+        a warning will be issued.
+
+        See also :ref:`language-versioning`.
+
+    .. versionchanged:: 2017.2.1
+
+        *lang_version* added.
+
     .. versionchanged:: 2017.2
 
         *fixed_parameters* added.
@@ -1953,6 +1970,24 @@ def make_kernel(domains, instructions, kernel_data=["..."], **kwargs):
     from loopy.options import make_options
     options = make_options(options)
 
+    lang_version = kwargs.pop("lang_version", None)
+    if lang_version is None:
+        from warnings import warn
+        from loopy.diagnostic import LoopyWarning
+        from loopy.version import (
+                MOST_RECENT_LANGUAGE_VERSION,
+                FALLBACK_LANGUAGE_VERSION)
+        warn("'lang_version' was not passed to make_kernel(). "
+                "To avoid this warning, pass "
+                "lang_version=%r in this invocation."
+                % (MOST_RECENT_LANGUAGE_VERSION,),
+                LoopyWarning, stacklevel=2)
+
+        lang_version = FALLBACK_LANGUAGE_VERSION
+
+    if lang_version >= (2018, 1):
+        options = options.copy(enforce_check_variable_access_ordered=True)
+
     if isinstance(silenced_warnings, str):
         silenced_warnings = silenced_warnings.split(";")
 
diff --git a/loopy/options.py b/loopy/options.py
index 13d0b752d..4277d999a 100644
--- a/loopy/options.py
+++ b/loopy/options.py
@@ -162,6 +162,14 @@ class Options(ImmutableRecord):
     .. rubric:: Features
 
     .. attribute:: disable_global_barriers
+
+    .. attribute:: enforce_check_variable_access_ordered
+
+        If *True*, require that
+        :func:`loopy.check.check_variable_access_ordered` passes.
+        Required for language versions 2018.1 and above. This check
+        helps find and eliminate unintentionally unordered access
+        to variables.
     """
 
     _legacy_options_map = {
@@ -216,6 +224,9 @@ class Options(ImmutableRecord):
                 disable_global_barriers=kwargs.get("disable_global_barriers",
                     False),
                 check_dep_resolution=kwargs.get("check_dep_resolution", True),
+
+                enforce_check_variable_access_ordered=kwargs.get(
+                    "enforce_check_variable_access_ordered", False),
                 )
 
     # {{{ legacy compatibility
diff --git a/loopy/version.py b/loopy/version.py
index 7141a6782..21c920ce4 100644
--- a/loopy/version.py
+++ b/loopy/version.py
@@ -33,3 +33,55 @@ else:
     _islpy_version = islpy.version.VERSION_TEXT
 
 DATA_MODEL_VERSION = "v76-islpy%s" % _islpy_version
+
+
+FALLBACK_LANGUAGE_VERSION = (2017, 2, 1)
+MOST_RECENT_LANGUAGE_VERSION = (2018, 1)
+
+__doc__ = """
+
+.. currentmodule:: loopy
+.. data:: VERSION
+
+    A tuple representing the current version number of loopy, for example
+    **(2017, 2, 1)**. Direct comparison of these tuples will always yield
+    valid version comparisons.
+
+.. _language-versioning:
+
+Loopy Language Versioning
+-------------------------
+
+At version 2018.1, :mod:`loopy` introduced a language versioning scheme to make
+it easier to evolve the language while retaining backward compatibility. What
+prompted this is the addition of
+:attr:`loopy.Options.enforce_check_variable_access_ordered`, which (despite
+its name) serves to enable a new check that helps ensure that all variable
+access in a kernel is ordered as intended. Since that has the potential to
+break existing programs, kernels now have to declare support for a given
+language version to let them take advantage of this check.
+
+As a result, :mod:`loopy` will now issue a warning when a call to
+:func:`loopy.make_kernel` does not declare a language version. Such kernels will
+(indefinitely) default to language version 2017.2.1.
+
+Language versions will generally reflect the version number of :mod:`loopy` in
+which they were introduced, though it is possible that some versions of
+:mod:`loopy` do not introduce new user-visible language features. In such
+situations, the previous language version number remains.
+
+
+.. data:: MOST_RECENT_LANGUAGE_VERSION
+
+    A tuple representing the most recent language version number of loopy, for
+    example **(2018, 1)**. Direct comparison of these tuples will always
+    yield valid version comparisons.
+
+History of Language Versions
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+* ``(2018, 1)``: :attr:`loopy.Options.enforce_check_variable_access_ordered`
+    is turned on by default.
+
+* ``(2017, 2, 1)``: Initial legacy language version.
+"""
diff --git a/test/test_dg.py b/test/test_dg.py
index d65c68ed4..ef4a31373 100644
--- a/test/test_dg.py
+++ b/test/test_dg.py
@@ -72,7 +72,8 @@ def test_dg_volume(ctx_factory):
                     order=order),
                 lp.ValueArg("K", np.int32, approximately=1000),
                 ],
-            name="dg_volume", assumptions="K>=1")
+            name="dg_volume", assumptions="K>=1",
+            lang_version=(2018, 1))
 
     knl = lp.fix_parameters(knl, Np=Np)
 
diff --git a/test/test_loopy.py b/test/test_loopy.py
index e36a4c2c3..02002c5cd 100644
--- a/test/test_loopy.py
+++ b/test/test_loopy.py
@@ -67,7 +67,8 @@ def test_globals_decl_once_with_multi_subprogram(ctx_factory):
             [lp.TemporaryVariable(
                 'cnst', shape=('n'), initializer=cnst,
                 scope=lp.temp_var_scope.GLOBAL,
-                read_only=True), '...'])
+                read_only=True), '...'],
+            lang_version=(2018, 1))
     knl = lp.fix_parameters(knl, n=16)
     knl = lp.add_barrier(knl, "id:first", "id:second")
 
@@ -88,7 +89,8 @@ def test_complicated_subst(ctx_factory):
                 h(x) := 1 + g(x) + 20*g$two(x)
 
                 a[i] = h$one(i) * h$two(i)
-                """)
+                """,
+            lang_version=(2018, 1))
 
     knl = lp.expand_subst(knl, "... > id:h and tag:two > id:g and tag:two")
 
@@ -119,7 +121,8 @@ def test_type_inference_no_artificial_doubles(ctx_factory):
                 lp.GlobalArg("c", np.float32, shape=("n",)),
                 lp.ValueArg("n", np.int32),
                 ],
-            assumptions="n>=1")
+            assumptions="n>=1",
+            lang_version=(2018, 1))
 
     knl = lp.preprocess_kernel(knl, ctx.devices[0])
     for k in lp.generate_loop_schedules(knl):
@@ -139,7 +142,9 @@ def test_type_inference_with_type_dependencies():
             c = b + c
             <>d = b + 2 + 1j
             """,
-            "...")
+            "...",
+            lang_version=(2018, 1))
+
     knl = lp.infer_unknown_types(knl)
 
     from loopy.types import to_loopy_type
-- 
GitLab