diff --git a/.ci-support/fix-code-for-docs.sh b/.ci-support/fix-code-for-docs.sh
new file mode 100755
index 0000000000000000000000000000000000000000..7d6da022e787ef34d092ff9cba5a74e4cf3b5915
--- /dev/null
+++ b/.ci-support/fix-code-for-docs.sh
@@ -0,0 +1,2 @@
+#! /bin/bash
+sed -i "s/Dict\[str, DataInterface\]/Dict/" pytato/codegen.py
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index ea72e3a1b8926360f9f0ec1f683cc937b5b428b0..cb8d11e97b9af10c5b4771664b409150514d82ec 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -64,10 +64,9 @@ Mypy:
 
 Documentation:
   script:
-  - EXTRA_INSTALL="sphinx-autodoc-typehints"
+  - EXTRA_INSTALL="pyopencl"
   - curl -L -O -k https://gitlab.tiker.net/inducer/ci-support/raw/master/build-docs.sh
+  - ./.ci-support/fix-code-for-docs.sh
   - ". ./build-docs.sh"
   tags:
   - python3
-  only:
-  - master
diff --git a/doc/conf.py b/doc/conf.py
index 96130ca4a3b32adc496db9cbae054a3e773b6749..5a61a6cecd67a1764725678072623f440345b427 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -31,9 +31,10 @@ extensions = [
     'sphinx.ext.autodoc',
     'sphinx.ext.intersphinx',
     'sphinx.ext.mathjax',
-    'sphinx_autodoc_typehints',
 ]
 
+autoclass_content = "class"
+
 # Add any paths that contain templates here, relative to this directory.
 templates_path = ['_templates']
 
@@ -55,15 +56,15 @@ html_theme = 'alabaster'
 html_static_path = []
 
 intersphinx_mapping = {
-    'http://docs.python.org/': None,
-    'http://documen.tician.de/boxtree/': None,
-    'http://docs.scipy.org/doc/numpy/': None,
-    'http://documen.tician.de/meshmode/': None,
-    'http://documen.tician.de/modepy/': None,
-    'http://documen.tician.de/pyopencl/': None,
-    'http://documen.tician.de/pytools/': None,
-    'http://documen.tician.de/pymbolic/': None,
-    'http://documen.tician.de/loopy/': None,
-    'http://documen.tician.de/sumpy/': None,
-    'http://documen.tician.de/islpy/': None,
+    'https://docs.python.org/3/': None,
+    'https://numpy.org/doc/stable/': None,
+    'https://documen.tician.de/boxtree/': None,
+    'https://documen.tician.de/meshmode/': None,
+    'https://documen.tician.de/modepy/': None,
+    'https://documen.tician.de/pyopencl/': None,
+    'https://documen.tician.de/pytools/': None,
+    'https://documen.tician.de/pymbolic/': None,
+    'https://documen.tician.de/loopy/': None,
+    'https://documen.tician.de/sumpy/': None,
+    'https://documen.tician.de/islpy/': None,
     }
diff --git a/doc/design.rst b/doc/design.rst
index 5114de67c23f18365d7bee7aba5deb54b94b1402..a569511ec98e9971a8b358a3821d76e66c8ae81e 100644
--- a/doc/design.rst
+++ b/doc/design.rst
@@ -71,7 +71,7 @@ Naming
 -   There is (for now) one :class:`~Namespace` per computation "universe" that defines
     the computational "environment", by mapping :term:`identifier`\ s to :term:`array expression`\ s
     (note: :class:`DictOfNamedArrays` instances may not be named, but their constituent
-    parts can, by using :class:`AttributeLookup`).
+    parts can, by using :class:`pytato.array.AttributeLookup`).
     Operations involving array expressions not using the same namespace are prohibited.
 
 -   Names in the :class:`~Namespace` are under user control and unique. I.e.
@@ -95,7 +95,7 @@ Naming
 
         A[(A > 0).tagged(CountNamed("mycount"))]
 
--   :class:`Placeholder` expressions, like all array expressions,
+-   :class:`pytato.array.Placeholder` expressions, like all array expressions,
     are considered read-only. When computation begins, the same
     actual memory may be supplied for multiple :term:`placeholder name`\ s,
     i.e. those arrays may alias.
@@ -135,7 +135,7 @@ Reserved Identifiers
 
     -   Identifiers matching the regular expression ``_in[0-9]+``. They are used
         as automatically generated names (if required) in
-        :attr:`IndexLambda.bindings`.
+        :attr:`pytato.array.IndexLambda.bindings`.
 
 Glossary
 ========
@@ -158,6 +158,6 @@ Glossary
         in a :class:`Namespace`.
 
     placeholder name
-        See :attr:`Placeholder.name`.
+        See :attr:`pytato.array.Placeholder.name`.
 
 .. vim: shiftwidth=4
diff --git a/doc/misc.rst b/doc/misc.rst
index c6ab9702dfebe1a3e902994148917b30b4dd5840..5c6a7d0878e3e98a36ce6a8af9cbc461ba236ced 100644
--- a/doc/misc.rst
+++ b/doc/misc.rst
@@ -26,3 +26,33 @@ 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.
 
+Cross-References for Other Documentation
+========================================
+.. class:: ellipsis
+
+    The type of the Python-builtin :data:`Ellipsis` object. (not otherwise
+    documented)
+
+.. currentmodule:: loopy.kernel
+
+.. class:: LoopKernel
+
+   See :class:`loopy.LoopKernel`.
+
+.. currentmodule:: loopy.options
+
+.. class:: Options
+
+   See :class:`loopy.Options`.
+
+.. currentmodule:: loopy.kernel.data
+
+.. class:: TemporaryVariable
+
+   See :class:`loopy.TemporaryVariable`.
+
+.. currentmodule:: islpy._isl
+
+.. class:: BasicSet
+
+   See :class:`islpy.BasicSet`.
diff --git a/doc/reference.rst b/doc/reference.rst
index 581f7b65b204ba8f2a1736ddc8d75d82e484ee34..5ee32af6b25683fcf743649f5027c4ad395d58ca 100644
--- a/doc/reference.rst
+++ b/doc/reference.rst
@@ -1,6 +1,8 @@
 Reference
 =========
 
+.. module:: pytato
+
 .. automodule:: pytato.array
 .. automodule:: pytato.scalar_expr
 .. automodule:: pytato.transform
diff --git a/pytato/array.py b/pytato/array.py
index 42070f5098e21facd7ddf7d550771cb6f84f4ee1..be55c4ecbd00cea69749bdb56738aae23a228b04 100644
--- a/pytato/array.py
+++ b/pytato/array.py
@@ -73,6 +73,7 @@ Pre-Defined Tags
 ----------------
 
 .. autoclass:: ImplementAs
+.. autoclass:: ImplementationStrategy
 .. autoclass:: CountNamed
 .. autoclass:: ImplStored
 .. autoclass:: ImplInlined
@@ -86,6 +87,7 @@ Built-in Expression Nodes
 .. autoclass:: MatrixProduct
 .. autoclass:: LoopyFunction
 .. autoclass:: Stack
+.. autoclass:: AttributeLookup
 
 Index Remapping
 ^^^^^^^^^^^^^^^
@@ -108,7 +110,7 @@ User-Facing Node Creation
 -------------------------
 
 Node constructors such as :class:`Placeholder.__init__` and
-:class:`DictOfNamedArrays.__init__` offer limited input validation
+:class:`~pytato.DictOfNamedArrays.__init__` offer limited input validation
 (in favor of faster execution). Node creation from outside
 :mod:`pytato` should use the following interfaces:
 
@@ -118,6 +120,33 @@ Node constructors such as :class:`Placeholder.__init__` and
 .. autofunction:: make_placeholder
 .. autofunction:: make_size_param
 .. autofunction:: make_data_wrapper
+
+Aliases
+-------
+
+(This section exists because Sphinx, our documentation tool, can't (yet)
+canonicalize type references. Once Sphinx 4.0 is released, we should use the
+``:canonical:`` option here.)
+
+.. class:: Namespace
+
+    Should be referenced as :class:`pytato.Namespace`.
+
+.. class:: DottedName
+
+    Should be referenced as :class:`pytato.DottedName`.
+
+.. class:: Tag
+
+    Should be referenced as :class:`pytato.Tag`.
+
+.. class:: Array
+
+    Should be referenced as :class:`pytato.Array`.
+
+.. class:: DictOfNamedArrays
+
+    Should be referenced as :class:`pytato.DictOfNamedArrays`.
 """
 
 # }}}
@@ -200,8 +229,8 @@ class Namespace(Mapping[str, "Array"]):
     r"""
     Represents a mapping from :term:`identifier` strings to
     :term:`array expression`\ s or *None*, where *None* indicates that the name
-    may not be used.  (:class:`Placeholder` instances register their names in
-    this way to avoid ambiguity.)
+    may not be used.  (:class:`pytato.array.Placeholder` instances register
+    their names in this way to avoid ambiguity.)
 
     .. attribute:: name_gen
     .. automethod:: __contains__
@@ -290,6 +319,8 @@ class Tag:
     of the form ``dotted.name(attr1=value1, attr2=value2)``.
     Positional arguments are not allowed.
 
+   .. automethod:: __repr__
+
    .. note::
 
        This mirrors the tagging scheme that :mod:`loopy`
@@ -419,7 +450,7 @@ class Array:
 
     .. attribute:: shape
 
-        Identifiers (:class:`pymbolic.Variable`) refer to names from
+        Identifiers (:class:`pymbolic.primitives.Variable`) refer to names from
         :attr:`namespace`.  A tuple of integers or :mod:`pymbolic` expressions.
         Shape may be (at most affinely) symbolic in these
         identifiers.
@@ -427,7 +458,8 @@ class Array:
         .. note::
 
             Affine-ness is mainly required by code generation for
-            :class:`IndexLambda`, but :class:`IndexLambda` is used to produce
+            :class:`~pytato.array.IndexLambda`, but
+            :class:`~pytato.array.IndexLambda` is used to produce
             references to named arrays. Since any array that needs to be
             referenced in this way needs to obey this restriction anyway,
             a decision was made to requir the same of *all* array expressions.
@@ -796,10 +828,11 @@ class DictOfNamedArrays(Mapping[str, Array]):
     to instances of :class:`Array`. May occur as a result
     type of array computations.
 
-    .. method:: __contains__
-    .. method:: __getitem__
-    .. method:: __iter__
-    .. method:: __len__
+    .. automethod:: __init__
+    .. automethod:: __contains__
+    .. automethod:: __getitem__
+    .. automethod:: __iter__
+    .. automethod:: __len__
 
     .. note::
 
@@ -833,6 +866,8 @@ class DictOfNamedArrays(Mapping[str, Array]):
 
 class IndexLambda(_SuppliedShapeAndDtypeMixin, Array):
     """
+    .. attribute:: namespace
+
     .. attribute:: expr
 
         A scalar-valued :mod:`pymbolic` expression such as
@@ -1021,13 +1056,25 @@ class Stack(Array):
 # }}}
 
 
+# {{{ attribute lookup
+
+class AttributeLookup(Array):
+    """An expression node to extract an array from a :class:`DictOfNamedArrays`.
+
+    .. warning::
+
+        Not yet implemented.
+    """
+    pass
+
+
 # {{{ index remapping
 
 class IndexRemappingBase(Array):
     """Base class for operations that remap the indices of an array.
 
     Note that index remappings can also be expressed via
-    :class:`~pytato.IndexLambda`.
+    :class:`~pytato.array.IndexLambda`.
 
     .. attribute:: array
 
@@ -1267,6 +1314,9 @@ class DataWrapper(InputArgumentBase):
 class Placeholder(_SuppliedShapeAndDtypeMixin, InputArgumentBase):
     r"""A named placeholder for an array whose concrete value is supplied by the
     user during evaluation.
+
+    .. attribute:: name
+    .. automethod:: __init__
     """
 
     _mapper_method = "map_placeholder"
@@ -1277,6 +1327,9 @@ class Placeholder(_SuppliedShapeAndDtypeMixin, InputArgumentBase):
             shape: ShapeType,
             dtype: np.dtype,
             tags: Optional[TagsType] = None):
+        """Should not be called directly. Use :func:`make_placeholder`
+        instead.
+        """
         super().__init__(shape=shape,
                 dtype=dtype,
                 namespace=namespace,
diff --git a/pytato/codegen.py b/pytato/codegen.py
index a55b669aed17a6f86aa9d0c5adaba44bfc91a584..496b1766bea9d02a88f9f3920eab3490b69dd534 100644
--- a/pytato/codegen.py
+++ b/pytato/codegen.py
@@ -47,6 +47,17 @@ from pytato.transform import Mapper, CopyMapper
 
 
 __doc__ = """
+References
+----------
+
+.. class:: DictOfNamedArrays
+
+    Should be referenced as :class:`pytato.DictOfNamedArrays`.
+
+.. class:: DataInterface
+
+    Should be referenced as :class:`pytato.array.DataInterface`.
+
 Generating Code
 ---------------
 
@@ -78,8 +89,9 @@ Code Generation Internals
 .. autofunction:: rename_reductions
 .. autofunction:: normalize_outputs
 .. autofunction:: get_initial_codegen_state
-.. autofunction:: preprocess
 
+.. autoclass:: PreprocessResult
+.. autofunction:: preprocess
 """
 
 
diff --git a/pytato/program.py b/pytato/program.py
index ded19094ad9db8fff75ce722b7a5aacc37777e32..e2e2c867667665094e5d3bcddbec679b27d108cc 100644
--- a/pytato/program.py
+++ b/pytato/program.py
@@ -40,8 +40,8 @@ from typing import Any, Mapping, Optional
 if typing.TYPE_CHECKING:
     # Imports skipped for efficiency.  FIXME: Neither of these work as type
     # stubs are not present. Types are here only as documentation.
-    import pyopencl as cl
-    import loopy as lp
+    import pyopencl
+    import loopy
     # Imports skipped to avoid circular dependencies.
     import pytato.target
 
@@ -65,7 +65,7 @@ class BoundProgram:
     .. automethod:: __call__
     """
 
-    program: "lp.LoopKernel"
+    program: "loopy.LoopKernel"
     bound_arguments: Mapping[str, Any]
     target: "pytato.target.Target"
 
@@ -83,7 +83,7 @@ class BoundPyOpenCLProgram(BoundProgram):
 
     .. automethod:: __call__
     """
-    queue: Optional["cl.CommandQueue"]
+    queue: Optional["pyopencl.CommandQueue"]
 
     def __call__(self, *args: Any, **kwargs: Any) -> Any:
         """Convenience function for launching a :mod:`pyopencl` computation."""
diff --git a/pytato/target.py b/pytato/target.py
index 0242c0d07475fe4dad1e6f394c00a660ad170358..72e819b66e6390af0fb2e9f14a875e5ca39e63b8 100644
--- a/pytato/target.py
+++ b/pytato/target.py
@@ -32,17 +32,12 @@ Code Generation Targets
 .. autoclass:: PyOpenCLTarget
 """
 
-import typing
 from typing import Any, Mapping, Optional
 
 from pytato.program import BoundProgram, BoundPyOpenCLProgram
 
-
-if typing.TYPE_CHECKING:
-    # Skip imports for efficiency.  FIXME: Neither of these work as type stubs
-    # are not present. Types are here only as documentation.
-    import pyopencl as cl
-    import loopy as lp
+import pyopencl
+import loopy
 
 
 class Target:
@@ -52,11 +47,11 @@ class Target:
     .. automethod:: bind_program
     """
 
-    def get_loopy_target(self) -> "lp.TargetBase":
+    def get_loopy_target(self) -> "loopy.TargetBase":
         """Return the corresponding :mod:`loopy` target."""
         raise NotImplementedError
 
-    def bind_program(self, program: "lp.LoopKernel",
+    def bind_program(self, program: "loopy.LoopKernel",
             bound_arguments: Mapping[str, Any]) -> BoundProgram:
         """Create a :class:`pytato.program.BoundProgram` for this code generation target.
 
@@ -74,17 +69,17 @@ class PyOpenCLTarget(Target):
         The :mod:`pyopencl` command queue, or *None*.
     """
 
-    def __init__(self, queue: Optional["cl.CommandQueue"] = None):
+    def __init__(self, queue: Optional["pyopencl.CommandQueue"] = None):
         self.queue = queue
 
-    def get_loopy_target(self) -> "lp.PyOpenCLTarget":
+    def get_loopy_target(self) -> "loopy.PyOpenCLTarget":
         import loopy as lp
         device = None
         if self.queue is not None:
             device = self.queue.device
         return lp.PyOpenCLTarget(device)
 
-    def bind_program(self, program: "lp.LoopKernel",
+    def bind_program(self, program: "loopy.LoopKernel",
             bound_arguments: Mapping[str, Any]) -> BoundProgram:
         return BoundPyOpenCLProgram(program=program,
                 queue=self.queue,