diff --git a/grudge/execution.py b/grudge/execution.py
index e3646b7361d8b1606a1a234fbe6089a51127f8b7..aa60715dd5f7ecd36b3380654bc9654afdb63930 100644
--- a/grudge/execution.py
+++ b/grudge/execution.py
@@ -333,11 +333,11 @@ class ExecutionMapper(mappers.Evaluator,
 
     # {{{ code execution functions
 
-    def exec_assign(self, insn):
+    def map_insn_assign(self, insn):
         return [(name, self.rec(expr))
                 for name, expr in zip(insn.names, insn.exprs)], []
 
-    def exec_assign_to_discr_scoped(self, insn):
+    def map_insn_assign_to_discr_scoped(self, insn):
         assignments = []
         for name, expr in zip(insn.names, insn.exprs):
             value = self.rec(expr)
@@ -346,11 +346,11 @@ class ExecutionMapper(mappers.Evaluator,
 
         return assignments, []
 
-    def exec_assign_from_discr_scoped(self, insn):
+    def map_insn_assign_from_discr_scoped(self, insn):
         return [(insn.name,
             self.discr._discr_scoped_subexpr_name_to_value[insn.name])], []
 
-    def exec_diff_batch_assign(self, insn):
+    def map_insn_diff_batch_assign(self, insn):
         field = self.rec(insn.field)
         repr_op = insn.operators[0]
         # FIXME: There's no real reason why differentiation is special,
diff --git a/grudge/symbolic/compiler.py b/grudge/symbolic/compiler.py
index fc4ad5f016c522c4c362190dca4ae76aedd5cbd5..7259e4e42e4959472265a6097e9c49cf09a8ba79 100644
--- a/grudge/symbolic/compiler.py
+++ b/grudge/symbolic/compiler.py
@@ -31,6 +31,7 @@ from pytools import Record, memoize_method, memoize
 from grudge import sym
 import grudge.symbolic.mappers as mappers
 from pymbolic.primitives import Variable, Subscript
+from sys import intern
 
 
 # {{{ instructions
@@ -38,6 +39,7 @@ from pymbolic.primitives import Variable, Subscript
 class Instruction(Record):
     __slots__ = []
     priority = 0
+    neglect_for_dofdesc_inference = False
 
     def get_assignees(self):
         raise NotImplementedError("no get_assignees in %s" % self.__class__)
@@ -48,9 +50,6 @@ class Instruction(Record):
     def __str__(self):
         raise NotImplementedError
 
-    def get_execution_method(self, exec_mapper):
-        raise NotImplementedError
-
     def __hash__(self):
         return id(self)
 
@@ -69,6 +68,27 @@ def _make_dep_mapper(include_subscripts):
             include_calls="descend_args")
 
 
+# {{{ loopy kernel instruction
+
+class LoopyKernelDescriptor(object):
+    def __init__(self, loopy_kernel, input_mappings, output_mappings,
+            fixed_arguments):
+        self.loopy_kernel = loopy_kernel
+        self.input_mappings = input_mappings
+        self.output_mappings = output_mappings
+        self.fixed_arguments = fixed_arguments
+
+
+class LoopyKernelInstruction(Instruction):
+    comment = ""
+    scope_indicator = ""
+
+    def __init__(self, per_group_kernel_descriptors):
+        self.per_group_kernel_descriptors = per_group_kernel_descriptors
+
+# }}}
+
+
 class AssignBase(Instruction):
     comment = ""
     scope_indicator = ""
@@ -143,19 +163,18 @@ class Assign(AssignBase):
 
         return deps
 
-    def get_execution_method(self, exec_mapper):
-        return exec_mapper.exec_assign
+    mapper_method = intern("map_insn_assign")
 
 
 class ToDiscretizationScopedAssign(Assign):
     scope_indicator = "(to discr)-"
 
-    def get_execution_method(self, exec_mapper):
-        return exec_mapper.exec_assign_to_discr_scoped
+    mapper_method = intern("map_insn_assign_to_discr_scoped")
 
 
 class FromDiscretizationScopedAssign(AssignBase):
     scope_indicator = "(discr)-"
+    neglect_for_dofdesc_inference = True
 
     def __init__(self, name, **kwargs):
         super(FromDiscretizationScopedAssign, self).__init__(name=name, **kwargs)
@@ -173,8 +192,7 @@ class FromDiscretizationScopedAssign(AssignBase):
     def __str__(self):
         return "%s <-(from discr)" % self.name
 
-    def get_execution_method(self, exec_mapper):
-        return exec_mapper.exec_assign_from_discr_scoped
+    mapper_method = intern("map_insn_assign_from_discr_scoped")
 
 
 class DiffBatchAssign(Instruction):
@@ -212,57 +230,7 @@ class DiffBatchAssign(Instruction):
 
         return "\n".join(lines)
 
-    def get_execution_method(self, exec_mapper):
-        return exec_mapper.exec_diff_batch_assign
-
-
-class FluxExchangeBatchAssign(Instruction):
-    """
-    .. attribute:: names
-    .. attribute:: indices_and_ranks
-    .. attribute:: rank_to_index_and_name
-    .. attribute:: arg_fields
-    """
-
-    priority = 1
-
-    def __init__(self, names, indices_and_ranks, arg_fields):
-        rank_to_index_and_name = {}
-        for name, (index, rank) in zip(
-                names, indices_and_ranks):
-            rank_to_index_and_name.setdefault(rank, []).append(
-                (index, name))
-
-        Instruction.__init__(self,
-                names=names,
-                indices_and_ranks=indices_and_ranks,
-                rank_to_index_and_name=rank_to_index_and_name,
-                arg_fields=arg_fields)
-
-    def get_assignees(self):
-        return set(self.names)
-
-    @memoize_method
-    def get_dependencies(self):
-        dep_mapper = _make_dep_mapper()
-        result = set()
-        for fld in self.arg_fields:
-            result |= dep_mapper(fld)
-        return result
-
-    def __str__(self):
-        lines = []
-
-        lines.append("{")
-        for n, (index, rank) in zip(self.names, self.indices_and_ranks):
-            lines.append("  %s <- receive index %s from rank %d [%s]" % (
-                n, index, rank, self.arg_fields))
-        lines.append("}")
-
-        return "\n".join(lines)
-
-    def get_execution_method(self, exec_mapper):
-        return exec_mapper.exec_flux_exchange_batch_assign
+    mapper_method = intern("map_insn_diff_batch_assign")
 
 # }}}
 
@@ -484,8 +452,8 @@ class Code(object):
                         del context[name]
 
                     done_insns.add(insn)
-                    assignments, new_futures = \
-                            insn.get_execution_method(exec_mapper)(insn)
+                    mapper_method = getattr(exec_mapper, insn.mapper_method)
+                    assignments, new_futures = mapper_method(insn)
 
             if insn is not None:
                 for target, value in assignments:
@@ -636,6 +604,9 @@ class OperatorCompiler(mappers.IdentityMapper):
         # Finally, walk the expression and build the code.
         result = super(OperatorCompiler, self).__call__(expr, codegen_state)
 
+        from grudge.symbolic.dofdesc_inference import DOFDescInferenceMapper
+        inf_mapper = DOFDescInferenceMapper(self.discr_code + self.eval_code)
+
         from pytools.obj_array import make_obj_array
         return (
                 Code(self.discr_code,
@@ -643,9 +614,7 @@ class OperatorCompiler(mappers.IdentityMapper):
                         [Variable(name)
                             for name in self.discr_scope_names_copied_to_eval])),
                 Code(
-                    # FIXME: Enable
-                    #self.aggregate_assignments(self.eval_code, result),
-                    self.eval_code,
+                    self.aggregate_assignments(inf_mapper, self.eval_code, result),
                     result))
 
     # }}}
@@ -799,7 +768,7 @@ class OperatorCompiler(mappers.IdentityMapper):
 
     # {{{ assignment aggregration pass
 
-    def aggregate_assignments(self, instructions, result):
+    def aggregate_assignments(self, inf_mapper, instructions, result):
         from pymbolic.primitives import Variable
 
         # {{{ aggregation helpers
@@ -853,9 +822,16 @@ class OperatorCompiler(mappers.IdentityMapper):
                     for assignee in insn.get_assignees())
 
         from pytools import partition
+        from grudge.symbolic.primitives import DTAG_SCALAR
+
         unprocessed_assigns, other_insns = partition(
-                # FIXME: Re-add check for scalar result, exclude
-                lambda insn: isinstance(insn, Assign),
+                lambda insn: (
+                    isinstance(insn, Assign)
+                    and not isinstance(insn, ToDiscretizationScopedAssign)
+                    and not isinstance(insn, FromDiscretizationScopedAssign)
+                    and not any(
+                        inf_mapper.infer_for_name(n).domain_tag == DTAG_SCALAR
+                        for n in insn.names)),
                 instructions)
 
         # filter out zero-flop-count assigns--no need to bother with those
@@ -864,7 +840,6 @@ class OperatorCompiler(mappers.IdentityMapper):
                 unprocessed_assigns)
 
         # filter out zero assignments
-        from pytools import any
         from grudge.tools import is_zero
 
         i = 0
@@ -872,7 +847,7 @@ class OperatorCompiler(mappers.IdentityMapper):
         while i < len(unprocessed_assigns):
             my_assign = unprocessed_assigns[i]
             if any(is_zero(expr) for expr in my_assign.exprs):
-                processed_assigns.append(unprocessed_assigns.pop())
+                processed_assigns.append(unprocessed_assigns.pop(i))
             else:
                 i += 1
 
diff --git a/grudge/symbolic/dofdesc_inference.py b/grudge/symbolic/dofdesc_inference.py
new file mode 100644
index 0000000000000000000000000000000000000000..6b8cbebb75f0d4ee0500553d955c8ad40f072376
--- /dev/null
+++ b/grudge/symbolic/dofdesc_inference.py
@@ -0,0 +1,218 @@
+from __future__ import division, absolute_import
+
+__copyright__ = "Copyright (C) 2017 Andreas Kloeckner"
+
+__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.
+"""
+
+
+# This is purely leaves-to-roots. No need to propagate information in the
+# opposite direction.
+
+
+from pymbolic.mapper import RecursiveMapper, CSECachingMapperMixin
+from grudge.symbolic.primitives import DOFDesc, DTAG_SCALAR
+
+
+def unify_dofdescs(dd_a, dd_b, expr=None):
+    if dd_a is None:
+        assert dd_b is not None
+        return dd_b
+
+    if expr is not None:
+        loc_str = "in expression %s" % str(expr)
+    else:
+        loc_str = ""
+
+    from grudge.symbolic.primitives import DTAG_SCALAR
+    if dd_a.domain_tag != dd_b.domain_tag:
+        if dd_a.domain_tag == DTAG_SCALAR:
+            return dd_b
+        elif dd_b.domain_tag == DTAG_SCALAR:
+            return dd_a
+        else:
+            raise ValueError("mismatched domain tags" + loc_str)
+
+    # domain tags match
+    if dd_a.quadrature_tag != dd_b.quadrature_tag:
+        raise ValueError("mismatched quadrature tags" + loc_str)
+
+    return dd_a
+
+
+class InferrableMultiAssignment(object):
+    """An assignemnt 'instruction' which may be used as part of type
+    inference.
+
+    .. method:: get_assignees(rec)
+
+        :returns: a :class:`set` of names which are assigned values by
+        this assignment.
+
+    .. method:: infer_dofdescs(rec)
+
+        :returns: a list of ``(name, :class:`grudge.symbolic.primitives.DOFDesc`)``
+        tuples, each indicating the value type of the value with *name*.
+    """
+
+    # (not a base class--only documents the interface)
+
+
+class DOFDescInferenceMapper(RecursiveMapper, CSECachingMapperMixin):
+    def __init__(self, assignments, name_to_dofdesc=None, check=True):
+        """
+        :arg assignments: a list of objects adhering to
+            :class:`InferrableMultiAssignment`.
+        :returns: an instance of :class:`DOFDescInferenceMapper`
+        """
+
+        self.check = check
+
+        self.name_to_assignment = dict(
+                (name, a)
+                for a in assignments
+                if not a.neglect_for_dofdesc_inference
+                for name in a.get_assignees())
+
+        if name_to_dofdesc is None:
+            name_to_dofdesc = {}
+        else:
+            name_to_dofdesc = name_to_dofdesc.copy()
+
+        self.name_to_dofdesc = name_to_dofdesc
+
+    def infer_for_name(self, name):
+        try:
+            return self.name_to_dofdesc[name]
+        except KeyError:
+            a = self.name_to_assignment[name]
+
+            inf_method = getattr(self, a.mapper_method)
+            for r_name, r_dofdesc in inf_method(a):
+                assert r_name not in self.name_to_dofdesc
+                self.name_to_dofdesc[r_name] = r_dofdesc
+
+            return self.name_to_dofdesc[name]
+
+    # {{{ expression mappings
+
+    def map_constant(self, expr):
+        return DOFDesc(DTAG_SCALAR)
+
+    def map_grudge_variable(self, expr):
+        return expr.dd
+
+    def map_variable(self, expr):
+        return self.infer_for_name(expr.name)
+
+    def map_subscript(self, expr):
+        # FIXME: Subscript has same type as aggregate--a bit weird
+        return self.rec(expr.aggregate)
+
+    def map_arithmetic(self, expr, children):
+        dofdesc = None
+
+        for ch in children:
+            dofdesc = unify_dofdescs(dofdesc, self.rec(ch), expr)
+
+        if dofdesc is None:
+            raise ValueError("no DOFDesc found for expression %s" % expr)
+        else:
+            return dofdesc
+
+    def map_sum(self, expr):
+        return self.map_arithmetic(expr, expr.children)
+
+    map_product = map_sum
+
+    def map_quotient(self, expr):
+        return self.map_arithmetic(expr, (expr.numerator, expr.denominator))
+
+    def map_power(self, expr):
+        return self.map_arithmetic(expr, (expr.base, expr.exponent))
+
+    def map_nodal_sum(self, expr, enclosing_prec):
+        return DOFDesc(DTAG_SCALAR)
+
+    map_nodal_max = map_nodal_sum
+    map_nodal_min = map_nodal_sum
+
+    def map_operator_binding(self, expr):
+        operator = expr.op
+
+        if self.check:
+            op_dd = self.rec(expr.field)
+            if op_dd != operator.dd_in:
+                raise ValueError("mismatched input to %s "
+                        "(got: %s, expected: %s)"
+                        " in '%s'"
+                        % (
+                            type(expr).__name__,
+                            op_dd, expr.dd_in,
+                            str(expr)))
+
+        return operator.dd_out
+
+    def map_ones(self, expr):
+        return expr.dd
+
+    map_node_coordinate_component = map_ones
+
+    def map_call(self, expr):
+        arg_dds = [
+                self.rec(par)
+                for par in expr.parameters]
+
+        assert arg_dds
+
+        # FIXME
+        return arg_dds[0]
+
+    # }}}
+
+    # {{{ instruction mappings
+
+    def map_insn_assign(self, insn):
+        return [
+                (name, self.rec(expr))
+                for name, expr in zip(insn.names, insn.exprs)
+                ]
+
+    map_insn_assign_to_discr_scoped = map_insn_assign
+
+    def map_insn_diff_batch_assign(self, insn):
+        if self.check:
+            repr_op = insn.operators[0]
+            input_dd = self.rec(insn.field)
+            if input_dd != repr_op.dd_in:
+                raise ValueError("mismatched input to %s "
+                        "(got: %s, expected: %s)"
+                        % (
+                            type(insn).__name__,
+                            input_dd, repr_op.dd_in,
+                            ))
+
+        return [
+                (name, op.dd_out)
+                for name, op in zip(insn.names, insn.operators)]
+
+    # }}}
+
+# vim: foldmethod=marker
diff --git a/grudge/symbolic/primitives.py b/grudge/symbolic/primitives.py
index 0db0a8908a2b9647c87e15046480de8d3904fbe5..b1cf4a54b90671a4c073eba4c3c7547b2a38b05a 100644
--- a/grudge/symbolic/primitives.py
+++ b/grudge/symbolic/primitives.py
@@ -191,6 +191,9 @@ class DOFDesc(object):
         if domain_tag is DTAG_SCALAR and quadrature_tag is not None:
             raise ValueError("cannot have nontrivial quadrature tag on scalar")
 
+        if quadrature_tag is None:
+            quadrature_tag = QTAG_NONE
+
         self.domain_tag = domain_tag
         self.quadrature_tag = quadrature_tag