diff --git a/arraycontext/__init__.py b/arraycontext/__init__.py
index 3e0092db314c733339fbefbffcb140f37c32dc8e..a338059d1caa3e27d680321b5e4c58da51308aa3 100644
--- a/arraycontext/__init__.py
+++ b/arraycontext/__init__.py
@@ -28,9 +28,14 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 THE SOFTWARE.
 """
 
+import sys
 from .context import ArrayContext
 
-from .metadata import CommonSubexpressionTag, FirstAxisIsElementsTag
+from .transform_metadata import (CommonSubexpressionTag,
+        ElementwiseMapKernelTag)
+
+# deprecated, remove in 2022.
+from .metadata import _FirstAxisIsElementsTag
 
 from .container import (
         ArrayContainer,
@@ -65,7 +70,7 @@ __all__ = (
         "ArrayContext",
 
         "CommonSubexpressionTag",
-        "FirstAxisIsElementsTag",
+        "ElementwiseMapKernelTag",
 
         "ArrayContainer",
         "is_array_container", "is_array_container_type",
@@ -91,7 +96,9 @@ __all__ = (
         )
 
 
-def _acf():
+# {{{ deprecation handling
+
+def _deprecated_acf():
     """A tiny undocumented function to pass to tests that take an ``actx_factory``
     argument when running them from the command line.
     """
@@ -101,4 +108,32 @@ def _acf():
     queue = cl.CommandQueue(context)
     return PyOpenCLArrayContext(queue)
 
+
+_depr_name_to_replacement_and_obj = {
+        "FirstAxisIsElementsTag":
+        ("meshmode.transform_metadata.FirstAxisIsElementsTag",
+            _FirstAxisIsElementsTag),
+        "_acf":
+        ("<no replacement yet>", _deprecated_acf),
+        }
+
+if sys.version_info >= (3, 7):
+    def __getattr__(name):
+        replacement_and_obj = _depr_name_to_replacement_and_obj.get(name, None)
+        if replacement_and_obj is not None:
+            replacement, obj = replacement_and_obj
+            from warnings import warn
+            warn(f"'arraycontext.{name}' is deprecated. "
+                    f"Use '{replacement}' instead. "
+                    f"'arraycontext.{name}' will continue to work until 2022.",
+                    DeprecationWarning, stacklevel=2)
+            return obj
+        else:
+            raise AttributeError(name)
+else:
+    FirstAxisIsElementsTag = _FirstAxisIsElementsTag
+    _acf = _deprecated_acf
+
+# }}}
+
 # vim: foldmethod=marker
diff --git a/arraycontext/context.py b/arraycontext/context.py
index 5afc942a3936e9355c2b5b7afd5df65e0c0cfd02..04277f1ccefb29f502df6b004b77a385d0a02be5 100644
--- a/arraycontext/context.py
+++ b/arraycontext/context.py
@@ -219,6 +219,7 @@ class ArrayContext(ABC):
 
         import loopy as lp
         from .loopy import make_loopy_program
+        from arraycontext.transform_metadata import ElementwiseMapKernelTag
         return make_loopy_program(
                 [domain_bset],
                 [
@@ -227,7 +228,8 @@ class ArrayContext(ABC):
                         var(c_name)(*[
                             var("inp%d" % i)[subscript] for i in range(nargs)]))
                     ],
-                name="actx_special_%s" % c_name)
+                name="actx_special_%s" % c_name,
+                tags=(ElementwiseMapKernelTag(),))
 
     @abstractmethod
     def freeze(self, array):
diff --git a/arraycontext/impl/pyopencl/__init__.py b/arraycontext/impl/pyopencl/__init__.py
index 788dcdbe2d7096e16d2748104b008f4bfdd38a3a..04319027d8a8d2415fdd0edb41063415356bca3c 100644
--- a/arraycontext/impl/pyopencl/__init__.py
+++ b/arraycontext/impl/pyopencl/__init__.py
@@ -34,7 +34,6 @@ import numpy as np
 
 from pytools.tag import Tag
 
-from arraycontext.metadata import FirstAxisIsElementsTag
 from arraycontext.context import ArrayContext
 
 
@@ -101,8 +100,11 @@ class PyOpenCLArrayContext(ArrayContext):
             to the host.
         """
         if not force_device_scalars:
-            warn("Returning host scalars from the array context is deprecated. "
-                    "To return device scalars set 'force_device_scalars=True'. "
+            warn("Configuring the PyOpenCLArrayContext to return host scalars "
+                    "from reductions is deprecated. "
+                    "To configure the PyOpenCLArrayContext to return "
+                    "device scalars, pass 'force_device_scalars=True' to the "
+                    "constructor. "
                     "Support for returning host scalars will be removed in 2022.",
                     DeprecationWarning, stacklevel=2)
 
@@ -165,7 +167,10 @@ class PyOpenCLArrayContext(ArrayContext):
         try:
             t_unit = self._loopy_transform_cache[t_unit]
         except KeyError:
+            orig_t_unit = t_unit
             t_unit = self.transform_loopy_program(t_unit)
+            self._loopy_transform_cache[orig_t_unit] = t_unit
+            del orig_t_unit
 
         evt, result = t_unit(self.queue, **kwargs, allocator=self.allocator)
 
@@ -190,11 +195,15 @@ class PyOpenCLArrayContext(ArrayContext):
     # }}}
 
     def transform_loopy_program(self, t_unit):
-        try:
-            return self._loopy_transform_cache[t_unit]
-        except KeyError:
-            pass
-        orig_t_unit = t_unit
+        from warnings import warn
+        warn("Using arraycontext.PyOpenCLArrayContext.transform_loopy_program "
+                "to transform a program. This is deprecated and will stop working "
+                "in 2022. Instead, subclass PyOpenCLArrayContext and implement "
+                "the specific logic required to transform the program for your "
+                "package or application. Check higher-level packages "
+                "(e.g. meshmode), which may already have subclasses you may want "
+                "to build on.",
+                DeprecationWarning, stacklevel=2)
 
         # accommodate loopy with and without kernel callables
 
@@ -210,9 +219,13 @@ class PyOpenCLArrayContext(ArrayContext):
         all_inames = default_entrypoint.all_inames()
         # FIXME: This could be much smarter.
         inner_iname = None
+
+        # import with underscore to avoid DeprecationWarning
+        from arraycontext.metadata import _FirstAxisIsElementsTag
+
         if (len(default_entrypoint.instructions) == 1
                 and isinstance(default_entrypoint.instructions[0], lp.Assignment)
-                and any(isinstance(tag, FirstAxisIsElementsTag)
+                and any(isinstance(tag, _FirstAxisIsElementsTag)
                     # FIXME: Firedrake branch lacks kernel tags
                     for tag in getattr(default_entrypoint, "tags", ()))):
             stmt, = default_entrypoint.instructions
@@ -243,7 +256,6 @@ class PyOpenCLArrayContext(ArrayContext):
             t_unit = lp.split_iname(t_unit, inner_iname, 16, inner_tag="l.0")
         t_unit = lp.tag_inames(t_unit, {outer_iname: "g.0"})
 
-        self._loopy_transform_cache[orig_t_unit] = t_unit
         return t_unit
 
     def tag(self, tags: Union[Sequence[Tag], Tag], array):
diff --git a/arraycontext/loopy.py b/arraycontext/loopy.py
index 8f2816db58fe4b0a93a53542f2113c9cafbeea8a..f4c97754d731961baaaf0191f70dcfeca287b688 100644
--- a/arraycontext/loopy.py
+++ b/arraycontext/loopy.py
@@ -39,7 +39,7 @@ _DEFAULT_LOOPY_OPTIONS = lp.Options(
 
 
 def make_loopy_program(domains, statements, kernel_data=None,
-        name="mm_actx_kernel"):
+        name="mm_actx_kernel", tags=None):
     """Return a :class:`loopy.LoopKernel` suitable for use with
     :meth:`ArrayContext.call_loopy`.
     """
@@ -53,7 +53,8 @@ def make_loopy_program(domains, statements, kernel_data=None,
             options=_DEFAULT_LOOPY_OPTIONS,
             default_offset=lp.auto,
             name=name,
-            lang_version=MOST_RECENT_LANGUAGE_VERSION)
+            lang_version=MOST_RECENT_LANGUAGE_VERSION,
+            tags=tags)
 
 
 def get_default_entrypoint(t_unit):
diff --git a/arraycontext/metadata.py b/arraycontext/metadata.py
index 1713cd1b4f1fc944867cefca0ef13f8afda711e5..6291a86504563c64faf63be79467784ff65a6cfa 100644
--- a/arraycontext/metadata.py
+++ b/arraycontext/metadata.py
@@ -1,8 +1,3 @@
-"""
-.. autoclass:: CommonSubexpressionTag
-.. autoclass:: FirstAxisIsElementsTag
-"""
-
 __copyright__ = """
 Copyright (C) 2020-1 University of Illinois Board of Trustees
 """
@@ -27,24 +22,34 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 THE SOFTWARE.
 """
 
+import sys
 from pytools.tag import Tag
-
-
-# {{{ program metadata
-
-class CommonSubexpressionTag(Tag):
-    """A tag that is applicable to arrays indicating that this same array
-    may be evaluated multiple times, and that the implementation should
-    eliminate those redundant evaluations if possible.
-    """
-
-
-class FirstAxisIsElementsTag(Tag):
-    """A tag that is applicable to array outputs indicating that the
-    first index corresponds to element indices. This suggests that
-    the implementation should set element indices as the outermost
-    loop extent.
-    """
+from warnings import warn
+
+
+# {{{ deprecation handling
+
+try:
+    from meshmode.transform_metadata import FirstAxisIsElementsTag \
+            as _FirstAxisIsElementsTag
+except ImportError:
+    # placeholder in case meshmode is too old to have it.
+    class _FirstAxisIsElementsTag(Tag):  # type: ignore[no-redef]
+        pass
+
+
+if sys.version_info >= (3, 7):
+    def __getattr__(name):
+        if name == "FirstAxisIsElementsTag":
+            warn(f"'arraycontext.{name}' is deprecated. "
+                    f"Use 'meshmode.transform_metadata.{name}' instead. "
+                    f"'arraycontext.{name}' will continue to work until 2022.",
+                    DeprecationWarning, stacklevel=2)
+            return _FirstAxisIsElementsTag
+        else:
+            raise AttributeError(name)
+else:
+    FirstAxisIsElementsTag = _FirstAxisIsElementsTag
 
 # }}}
 
diff --git a/arraycontext/pytest.py b/arraycontext/pytest.py
index e56d903d9e06be576858b7d262cf9772868ef726..6f63a6cfdc555bf423fefba434dfc56fc2c1e03a 100644
--- a/arraycontext/pytest.py
+++ b/arraycontext/pytest.py
@@ -70,33 +70,39 @@ class PytestPyOpenCLArrayContextFactory:
         raise NotImplementedError
 
 
-class _PyOpenCLArrayContextFactory(PytestPyOpenCLArrayContextFactory):
+class _PytestPyOpenCLArrayContextFactoryWithClass(PytestPyOpenCLArrayContextFactory):
     force_device_scalars = True
 
-    def __call__(self):
+    @property
+    def actx_class(self):
         from arraycontext import PyOpenCLArrayContext
+        return PyOpenCLArrayContext
 
+    def __call__(self):
         # The ostensibly pointless assignment to *ctx* keeps the CL context alive
         # long enough to create the array context, which will then start
         # holding a reference to the context to keep it alive in turn.
         # On some implementations (notably Intel CPU), holding a reference
         # to a queue does not keep the context alive.
         ctx, queue = self.get_command_queue()
-        return PyOpenCLArrayContext(
+        return self.actx_class(
                 queue,
                 force_device_scalars=self.force_device_scalars)
 
     def __str__(self):
-        return ("<PyOpenCLArrayContext for <pyopencl.Device '%s' on '%s'>" %
-                (self.device.name.strip(),
-                 self.device.platform.name.strip()))
+        return ("<%s for <pyopencl.Device '%s' on '%s'>" %
+                (
+                    self.actx_class.__name__,
+                    self.device.name.strip(),
+                    self.device.platform.name.strip()))
 
 
-class _DeprecatedPyOpenCLArrayContextFactory(_PyOpenCLArrayContextFactory):
+class _PytestPyOpenCLArrayContextFactoryWithClassAndHostScalars(
+        _PytestPyOpenCLArrayContextFactoryWithClass):
     force_device_scalars = False
 
 
-class _PytatoPyOpenCLArrayContextFactory(PytestPyOpenCLArrayContextFactory):
+class _PytestPytatoPyOpenCLArrayContextFactory(PytestPyOpenCLArrayContextFactory):
     force_device_scalars = False
 
     def __call__(self):
@@ -112,13 +118,14 @@ class _PytatoPyOpenCLArrayContextFactory(PytestPyOpenCLArrayContextFactory):
 
 _ARRAY_CONTEXT_FACTORY_REGISTRY: \
         Dict[str, Type[PytestPyOpenCLArrayContextFactory]] = {
-                "pyopencl": _PyOpenCLArrayContextFactory,
-                "pyopencl-deprecated": _DeprecatedPyOpenCLArrayContextFactory,
-                "pytato-pyopencl": _PytatoPyOpenCLArrayContextFactory,
+                "pyopencl": _PytestPyOpenCLArrayContextFactoryWithClass,
+                "pyopencl-deprecated":
+                _PytestPyOpenCLArrayContextFactoryWithClassAndHostScalars,
+                "pytato-pyopencl": _PytestPytatoPyOpenCLArrayContextFactory,
                 }
 
 
-def register_array_context_factory(
+def register_pytest_array_context_factory(
         name: str,
         factory: Type[PytestPyOpenCLArrayContextFactory]) -> None:
     if name in _ARRAY_CONTEXT_FACTORY_REGISTRY:
@@ -274,6 +281,15 @@ def pytest_generate_tests_for_pyopencl_array_context(metafunc) -> None:
     for device selection.
     """
 
+    from warnings import warn
+    warn("pytest_generate_tests_for_pyopencl_array_context is deprecated. "
+            "Use 'pytest_generate_tests = "
+            "arraycontext.pytest_generate_tests_for_array_contexts"
+            "([\"pyopencl-deprecated\"])' instead. "
+            "pytest_generate_tests_for_pyopencl_array_context will stop working "
+            "in 2022.",
+            DeprecationWarning, stacklevel=2)
+
     pytest_generate_tests_for_array_contexts([
         "pyopencl-deprecated",
         ], factory_arg_name="actx_factory")(metafunc)
diff --git a/arraycontext/transform_metadata.py b/arraycontext/transform_metadata.py
new file mode 100644
index 0000000000000000000000000000000000000000..2e0942e95c674434af03ed3bff363aa26433d36c
--- /dev/null
+++ b/arraycontext/transform_metadata.py
@@ -0,0 +1,59 @@
+"""
+.. currentmodule:: arraycontext
+
+.. autoclass:: CommonSubexpressionTag
+.. autoclass:: ElementwiseMapKernelTag
+"""
+
+__copyright__ = """
+Copyright (C) 2020-1 University of Illinois Board of Trustees
+"""
+
+__license__ = """
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+"""
+
+from pytools.tag import Tag
+
+
+# {{{ program metadata
+
+class CommonSubexpressionTag(Tag):
+    """A tag that is applicable to arrays indicating that this same array
+    may be evaluated multiple times, and that the implementation should
+    eliminate those redundant evaluations if possible.
+    """
+
+
+class ElementwiseMapKernelTag(Tag):
+    """A tag that applies to :class:`loopy.LoopKernel` indicating that the kernel
+    is a "map", i.e. that the output array(s) has/have the same shape as the
+    input array(s), and that each output element only depends on its corresponding
+    element(s) in the input array(s).
+
+    .. note::
+
+        "Element" here refers to a scalar element of an array, not an element
+        in a finite-element discretization.
+    """
+
+# }}}
+
+
+# vim: foldmethod=marker
diff --git a/test/test_arraycontext.py b/test/test_arraycontext.py
index df732e717c657359572dc0ecb7fb2b09c9db39e1..e9d9d5eb244238cfc32b2d84b776085b4de9ac38 100644
--- a/test/test_arraycontext.py
+++ b/test/test_arraycontext.py
@@ -31,19 +31,49 @@ from arraycontext import (
         dataclass_array_container, with_container_arithmetic,
         serialize_container, deserialize_container,
         freeze, thaw,
-        FirstAxisIsElementsTag, ArrayContainer)
+        FirstAxisIsElementsTag,
+        PyOpenCLArrayContext,
+        ArrayContainer,)
 from arraycontext import (  # noqa: F401
         pytest_generate_tests_for_array_contexts,
         _acf)
+from arraycontext.pytest import (_PytestPyOpenCLArrayContextFactoryWithClass,
+                                 _PytestPytatoPyOpenCLArrayContextFactory)
+
 
 import logging
 logger = logging.getLogger(__name__)
 
 
+# {{{ array context fixture
+
+class _PyOpenCLArrayContextForTests(PyOpenCLArrayContext):
+    """Like :class:`PyOpenCLArrayContext`, but applies no program transformations
+    whatsoever. Only to be used for testing internal to :mod:`arraycontext`.
+    """
+
+    def transform_loopy_program(self, t_unit):
+        return t_unit
+
+
+class _PyOpenCLArrayContextWithHostScalarsForTestsFactory(
+        _PytestPyOpenCLArrayContextFactoryWithClass):
+    actx_class = _PyOpenCLArrayContextForTests
+
+
+class _PyOpenCLArrayContextForTestsFactory(
+        _PyOpenCLArrayContextWithHostScalarsForTestsFactory):
+    force_device_scalars = True
+
+
 pytest_generate_tests = pytest_generate_tests_for_array_contexts([
-    "pyopencl", "pyopencl-deprecated", "pytato-pyopencl"
+    _PyOpenCLArrayContextForTestsFactory,
+    _PyOpenCLArrayContextWithHostScalarsForTestsFactory,
+    _PytestPytatoPyOpenCLArrayContextFactory,
     ])
 
+# }}}
+
 
 # {{{ stand-in DOFArray implementation