diff --git a/examples/dagrt-fusion.py b/examples/dagrt-fusion.py
index 9ab70e2c535c64e62bdc17add11353b872ef0b2d..83350c4ea775864c17bf35ea10d0a1aa21dab91f 100755
--- a/examples/dagrt-fusion.py
+++ b/examples/dagrt-fusion.py
@@ -464,7 +464,7 @@ def dg_flux(c, tpair):
             np.dot(v.int, normal),
             u.int * normal) - flux_weak
 
-    return sym.interp(tpair.dd, "all_faces")(c*flux_strong)
+    return sym.project(tpair.dd, "all_faces")(c*flux_strong)
 
 
 def get_strong_wave_op_with_discr_direct(actx, dims=2, order=4):
@@ -503,8 +503,8 @@ def get_strong_wave_op_with_discr_direct(actx, dims=2, order=4):
 
     rad_normal = sym.normal(BTAG_ALL, dims)
 
-    rad_u = sym.cse(sym.interp("vol", BTAG_ALL)(u))
-    rad_v = sym.cse(sym.interp("vol", BTAG_ALL)(v))
+    rad_u = sym.cse(sym.project("vol", BTAG_ALL)(u))
+    rad_v = sym.cse(sym.project("vol", BTAG_ALL)(v))
 
     rad_bc = sym.cse(flat_obj_array(
             0.5*(rad_u - sign*np.dot(rad_normal, rad_v)),
@@ -635,7 +635,7 @@ class ExecutionMapperWithMemOpCounting(ExecutionMapperWrapper):
             profile_data["bytes_written"] = (
                     profile_data.get("bytes_written", 0) + result.nbytes)
 
-            if op.mapper_method == "map_interpolation":
+            if op.mapper_method == "map_projection":
                 profile_data["interp_bytes_read"] = (
                         profile_data.get("interp_bytes_read", 0) + field.nbytes)
                 profile_data["interp_bytes_written"] = (
@@ -657,7 +657,7 @@ class ExecutionMapperWithMemOpCounting(ExecutionMapperWrapper):
                     expr.op,
                     (
                         # TODO: Not comprehensive.
-                        sym_op.InterpolationOperator,
+                        sym_op.ProjectionOperator,
                         sym_op.RefFaceMassOperator,
                         sym_op.RefInverseMassOperator,
                         sym_op.OppositeInteriorFaceSwap)):
diff --git a/examples/wave/wave-eager.py b/examples/wave/wave-eager.py
index 7cab5e07d35fe7c597349314991066d7f7a4404c..ba1e4af7ff77dc214c7c03207415e16c09164ab6 100644
--- a/examples/wave/wave-eager.py
+++ b/examples/wave/wave-eager.py
@@ -63,15 +63,15 @@ def wave_flux(discr, c, w_tpair):
             0.5*normal_times(v_jump),
             )
 
-    return discr.interp(w_tpair.dd, "all_faces", c*flux_weak)
+    return discr.project(w_tpair.dd, "all_faces", c*flux_weak)
 
 
 def wave_operator(discr, c, w):
     u = w[0]
     v = w[1:]
 
-    dir_u = discr.interp("vol", BTAG_ALL, u)
-    dir_v = discr.interp("vol", BTAG_ALL, v)
+    dir_u = discr.project("vol", BTAG_ALL, u)
+    dir_v = discr.project("vol", BTAG_ALL, v)
     dir_bval = flat_obj_array(dir_u, dir_v)
     dir_bc = flat_obj_array(-dir_u, dir_v)
 
diff --git a/grudge/eager.py b/grudge/eager.py
index 05f1bb37fdc417e1dc16152302ccc7fc435504db..fce02c831c95c9cb4525aada624c7e15845b1fec 100644
--- a/grudge/eager.py
+++ b/grudge/eager.py
@@ -38,11 +38,18 @@ from grudge.symbolic.primitives import TracePair
 
 class EagerDGDiscretization(DGDiscretizationWithBoundaries):
     def interp(self, src, tgt, vec):
+        from warnings import warn
+        warn("using 'interp' is deprecated, use 'project' instead.",
+                DeprecationWarning, stacklevel=2)
+
+        return self.project(src, tgt, vec)
+
+    def project(self, src, tgt, vec):
         if (isinstance(vec, np.ndarray)
                 and vec.dtype.char == "O"
                 and not isinstance(vec, DOFArray)):
             return obj_array_vectorize(
-                    lambda el: self.interp(src, tgt, el), vec)
+                    lambda el: self.project(src, tgt, el), vec)
 
         return self.connection_from_dds(src, tgt)(vec)
 
diff --git a/grudge/execution.py b/grudge/execution.py
index 5d507bead594bebda7ad91114e0a9d14cde75115..1c25739d1964863fbda151851607aab6a746f09b 100644
--- a/grudge/execution.py
+++ b/grudge/execution.py
@@ -306,7 +306,7 @@ class ExecutionMapper(mappers.Evaluator,
 
         return result
 
-    def map_interpolation(self, op, field_expr):
+    def map_projection(self, op, field_expr):
         conn = self.discrwb.connection_from_dds(op.dd_in, op.dd_out)
         return conn(self.rec(field_expr))
 
diff --git a/grudge/models/advection.py b/grudge/models/advection.py
index 36ce5c155bde62a7314a3ec6f0d828e8968946a6..e160643d5232ca3e6d7c196afe39a6e96b9cf410 100644
--- a/grudge/models/advection.py
+++ b/grudge/models/advection.py
@@ -94,7 +94,7 @@ class StrongAdvectionOperator(AdvectionOperatorBase):
         u = sym.var("u")
 
         def flux(pair):
-            return sym.interp(pair.dd, "all_faces")(
+            return sym.project(pair.dd, "all_faces")(
                     self.flux(pair))
 
         return (
@@ -118,11 +118,11 @@ class WeakAdvectionOperator(AdvectionOperatorBase):
         u = sym.var("u")
 
         def flux(pair):
-            return sym.interp(pair.dd, "all_faces")(
+            return sym.project(pair.dd, "all_faces")(
                     self.flux(pair))
 
         bc_in = self.inflow_u
-        # bc_out = sym.interp("vol", self.outflow_tag)(u)
+        # bc_out = sym.project(sym.DD_VOLUME, self.outflow_tag)(u)
 
         return sym.InverseMassOperator()(
                 np.dot(
@@ -149,20 +149,20 @@ class VariableCoefficientAdvectionOperator(AdvectionOperatorBase):
         self.quad_tag = quad_tag
 
     def flux(self, u):
-        surf_v = sym.interp(sym.DD_VOLUME, u.dd)(self.v)
+        surf_v = sym.project(sym.DD_VOLUME, u.dd)(self.v)
         return advection_weak_flux(self.flux_type, u, surf_v)
 
     def sym_operator(self):
         u = sym.var("u")
 
         def flux(pair):
-            return sym.interp(pair.dd, face_dd)(self.flux(pair))
+            return sym.project(pair.dd, face_dd)(self.flux(pair))
 
         face_dd = sym.DOFDesc(sym.FACE_RESTR_ALL, self.quad_tag)
         boundary_dd = sym.DOFDesc(sym.BTAG_ALL, self.quad_tag)
         quad_dd = sym.DOFDesc(sym.DTAG_VOLUME_ALL, self.quad_tag)
 
-        to_quad = sym.interp(sym.DD_VOLUME, quad_dd)
+        to_quad = sym.project(sym.DD_VOLUME, quad_dd)
         stiff_t_op = sym.stiffness_t(self.ambient_dim,
                 dd_in=quad_dd, dd_out=sym.DD_VOLUME)
 
diff --git a/grudge/models/em.py b/grudge/models/em.py
index c3a651c4572ec7caf64ca8068d644826ebdc6cce..4810b4d0ebd8f023adc2eebf01dd62bc67957d6d 100644
--- a/grudge/models/em.py
+++ b/grudge/models/em.py
@@ -191,8 +191,8 @@ class MaxwellOperator(HyperbolicOperator):
         "Construct part of the flux operator template for PEC boundary conditions"
         e, h = self.split_eh(w)
 
-        pec_e = sym.cse(sym.interp("vol", self.pec_tag)(e))
-        pec_h = sym.cse(sym.interp("vol", self.pec_tag)(h))
+        pec_e = sym.cse(sym.project("vol", self.pec_tag)(e))
+        pec_h = sym.cse(sym.project("vol", self.pec_tag)(h))
 
         return flat_obj_array(-pec_e, pec_h)
 
@@ -200,8 +200,8 @@ class MaxwellOperator(HyperbolicOperator):
         "Construct part of the flux operator template for PMC boundary conditions"
         e, h = self.split_eh(w)
 
-        pmc_e = sym.cse(sym.interp("vol", self.pmc_tag)(e))
-        pmc_h = sym.cse(sym.interp("vol", self.pmc_tag)(h))
+        pmc_e = sym.cse(sym.project("vol", self.pmc_tag)(e))
+        pmc_h = sym.cse(sym.project("vol", self.pmc_tag)(h))
 
         return flat_obj_array(pmc_e, -pmc_h)
 
@@ -221,8 +221,8 @@ class MaxwellOperator(HyperbolicOperator):
         absorb_Z = (mu/epsilon)**0.5  # noqa: N806
         absorb_Y = 1/absorb_Z  # noqa: N806
 
-        absorb_e = sym.cse(sym.interp("vol", self.absorb_tag)(e))
-        absorb_h = sym.cse(sym.interp("vol", self.absorb_tag)(h))
+        absorb_e = sym.cse(sym.project("vol", self.absorb_tag)(e))
+        absorb_h = sym.cse(sym.project("vol", self.absorb_tag)(h))
 
         bc = flat_obj_array(
                 absorb_e + 1/2*(self.space_cross_h(absorb_normal, self.space_cross_e(
@@ -278,7 +278,7 @@ class MaxwellOperator(HyperbolicOperator):
                 ]
 
         def flux(pair):
-            return sym.interp(pair.dd, "all_faces")(self.flux(pair))
+            return sym.project(pair.dd, "all_faces")(self.flux(pair))
 
         return (
                 - self.local_derivatives(w)
diff --git a/grudge/models/wave.py b/grudge/models/wave.py
index 90f248d6556f6615733a361158ba573f4a6d9575..09a954e873573df374402c28762668573ae71882 100644
--- a/grudge/models/wave.py
+++ b/grudge/models/wave.py
@@ -113,8 +113,8 @@ class StrongWaveOperator(HyperbolicOperator):
         # boundary conditions -------------------------------------------------
 
         # dirichlet BCs -------------------------------------------------------
-        dir_u = sym.cse(sym.interp("vol", self.dirichlet_tag)(u))
-        dir_v = sym.cse(sym.interp("vol", self.dirichlet_tag)(v))
+        dir_u = sym.cse(sym.project("vol", self.dirichlet_tag)(u))
+        dir_v = sym.cse(sym.project("vol", self.dirichlet_tag)(v))
         if self.dirichlet_bc_f:
             # FIXME
             from warnings import warn
@@ -129,15 +129,15 @@ class StrongWaveOperator(HyperbolicOperator):
         dir_bc = sym.cse(dir_bc, "dir_bc")
 
         # neumann BCs ---------------------------------------------------------
-        neu_u = sym.cse(sym.interp("vol", self.neumann_tag)(u))
-        neu_v = sym.cse(sym.interp("vol", self.neumann_tag)(v))
+        neu_u = sym.cse(sym.project("vol", self.neumann_tag)(u))
+        neu_v = sym.cse(sym.project("vol", self.neumann_tag)(v))
         neu_bc = sym.cse(flat_obj_array(neu_u, -neu_v), "neu_bc")
 
         # radiation BCs -------------------------------------------------------
         rad_normal = sym.normal(self.radiation_tag, d)
 
-        rad_u = sym.cse(sym.interp("vol", self.radiation_tag)(u))
-        rad_v = sym.cse(sym.interp("vol", self.radiation_tag)(v))
+        rad_u = sym.cse(sym.project("vol", self.radiation_tag)(u))
+        rad_v = sym.cse(sym.project("vol", self.radiation_tag)(v))
 
         rad_bc = sym.cse(flat_obj_array(
                 0.5*(rad_u - self.sign*np.dot(rad_normal, rad_v)),
@@ -148,7 +148,7 @@ class StrongWaveOperator(HyperbolicOperator):
         nabla = sym.nabla(d)
 
         def flux(pair):
-            return sym.interp(pair.dd, "all_faces")(
+            return sym.project(pair.dd, "all_faces")(
                     self.flux(pair))
 
         result = (
@@ -253,8 +253,8 @@ class WeakWaveOperator(HyperbolicOperator):
         # boundary conditions -------------------------------------------------
 
         # dirichlet BCs -------------------------------------------------------
-        dir_u = sym.cse(sym.interp("vol", self.dirichlet_tag)(u))
-        dir_v = sym.cse(sym.interp("vol", self.dirichlet_tag)(v))
+        dir_u = sym.cse(sym.project("vol", self.dirichlet_tag)(u))
+        dir_v = sym.cse(sym.project("vol", self.dirichlet_tag)(v))
         if self.dirichlet_bc_f:
             # FIXME
             from warnings import warn
@@ -269,15 +269,15 @@ class WeakWaveOperator(HyperbolicOperator):
         dir_bc = sym.cse(dir_bc, "dir_bc")
 
         # neumann BCs ---------------------------------------------------------
-        neu_u = sym.cse(sym.interp("vol", self.neumann_tag)(u))
-        neu_v = sym.cse(sym.interp("vol", self.neumann_tag)(v))
+        neu_u = sym.cse(sym.project("vol", self.neumann_tag)(u))
+        neu_v = sym.cse(sym.project("vol", self.neumann_tag)(v))
         neu_bc = sym.cse(flat_obj_array(neu_u, -neu_v), "neu_bc")
 
         # radiation BCs -------------------------------------------------------
         rad_normal = sym.normal(self.radiation_tag, d)
 
-        rad_u = sym.cse(sym.interp("vol", self.radiation_tag)(u))
-        rad_v = sym.cse(sym.interp("vol", self.radiation_tag)(v))
+        rad_u = sym.cse(sym.project("vol", self.radiation_tag)(u))
+        rad_v = sym.cse(sym.project("vol", self.radiation_tag)(v))
 
         rad_bc = sym.cse(flat_obj_array(
                 0.5*(rad_u - self.sign*np.dot(rad_normal, rad_v)),
@@ -286,7 +286,7 @@ class WeakWaveOperator(HyperbolicOperator):
 
         # entire operator -----------------------------------------------------
         def flux(pair):
-            return sym.interp(pair.dd, "all_faces")(self.flux(pair))
+            return sym.project(pair.dd, "all_faces")(self.flux(pair))
 
         result = sym.InverseMassOperator()(
                 flat_obj_array(
@@ -397,9 +397,9 @@ class VariableCoefficientWeakWaveOperator(HyperbolicOperator):
         # boundary conditions -------------------------------------------------
 
         # dirichlet BCs -------------------------------------------------------
-        dir_c = sym.cse(sym.interp("vol", self.dirichlet_tag)(self.c))
-        dir_u = sym.cse(sym.interp("vol", self.dirichlet_tag)(u))
-        dir_v = sym.cse(sym.interp("vol", self.dirichlet_tag)(v))
+        dir_c = sym.cse(sym.project("vol", self.dirichlet_tag)(self.c))
+        dir_u = sym.cse(sym.project("vol", self.dirichlet_tag)(u))
+        dir_v = sym.cse(sym.project("vol", self.dirichlet_tag)(v))
         if self.dirichlet_bc_f:
             # FIXME
             from warnings import warn
@@ -414,28 +414,28 @@ class VariableCoefficientWeakWaveOperator(HyperbolicOperator):
         dir_bc = sym.cse(dir_bc, "dir_bc")
 
         # neumann BCs ---------------------------------------------------------
-        neu_c = sym.cse(sym.interp("vol", self.neumann_tag)(self.c))
-        neu_u = sym.cse(sym.interp("vol", self.neumann_tag)(u))
-        neu_v = sym.cse(sym.interp("vol", self.neumann_tag)(v))
+        neu_c = sym.cse(sym.project("vol", self.neumann_tag)(self.c))
+        neu_u = sym.cse(sym.project("vol", self.neumann_tag)(u))
+        neu_v = sym.cse(sym.project("vol", self.neumann_tag)(v))
         neu_bc = sym.cse(flat_obj_array(neu_c, neu_u, -neu_v), "neu_bc")
 
         # radiation BCs -------------------------------------------------------
         rad_normal = sym.normal(self.radiation_tag, d)
 
-        rad_c = sym.cse(sym.interp("vol", self.radiation_tag)(self.c))
-        rad_u = sym.cse(sym.interp("vol", self.radiation_tag)(u))
-        rad_v = sym.cse(sym.interp("vol", self.radiation_tag)(v))
+        rad_c = sym.cse(sym.project("vol", self.radiation_tag)(self.c))
+        rad_u = sym.cse(sym.project("vol", self.radiation_tag)(u))
+        rad_v = sym.cse(sym.project("vol", self.radiation_tag)(v))
 
         rad_bc = sym.cse(flat_obj_array(rad_c,
-                0.5*(rad_u - sym.interp("vol", self.radiation_tag)(self.sign)
+                0.5*(rad_u - sym.project("vol", self.radiation_tag)(self.sign)
                     * np.dot(rad_normal, rad_v)),
                 0.5*rad_normal*(np.dot(rad_normal, rad_v)
-                    - sym.interp("vol", self.radiation_tag)(self.sign)*rad_u)
+                    - sym.project("vol", self.radiation_tag)(self.sign)*rad_u)
                 ), "rad_bc")
 
         # entire operator -----------------------------------------------------
         def flux(pair):
-            return sym.interp(pair.dd, "all_faces")(self.flux(pair))
+            return sym.project(pair.dd, "all_faces")(self.flux(pair))
 
         result = sym.InverseMassOperator()(
                 flat_obj_array(
diff --git a/grudge/symbolic/mappers/__init__.py b/grudge/symbolic/mappers/__init__.py
index 36b57d50aaf333fbd3303fd72f1425f5de085086..6d060e4691256404e12b600f9dfa6236f1216e7d 100644
--- a/grudge/symbolic/mappers/__init__.py
+++ b/grudge/symbolic/mappers/__init__.py
@@ -131,7 +131,7 @@ class OperatorReducerMixin(LocalOpReducerMixin, FluxOpReducerMixin):
 
     map_elementwise_linear = _map_op_base
 
-    map_interpolation = _map_op_base
+    map_projection = _map_op_base
 
     map_elementwise_sum = _map_op_base
     map_elementwise_min = _map_op_base
@@ -184,7 +184,7 @@ class IdentityMapperMixin(LocalOpReducerMixin, FluxOpReducerMixin):
         # it's a leaf--no changing children
         return expr
 
-    map_interpolation = map_elementwise_linear
+    map_projection = map_elementwise_linear
 
     map_elementwise_sum = map_elementwise_linear
     map_elementwise_min = map_elementwise_linear
@@ -358,9 +358,9 @@ class DOFDescReplacer(IdentityMapper):
             field = self.rec(expr.field)
             return op.OppositePartitionFaceSwap(dd_in=self.new_dd,
                                                 dd_out=self.new_dd)(field)
-        elif (isinstance(expr.op, op.InterpolationOperator)
+        elif (isinstance(expr.op, op.ProjectionOperator)
                     and expr.op.dd_out == self.prev_dd):
-            return op.InterpolationOperator(dd_in=expr.op.dd_in,
+            return op.ProjectionOperator(dd_in=expr.op.dd_in,
                                             dd_out=self.new_dd)(expr.field)
         elif (isinstance(expr.op, op.RefDiffOperatorBase)
                     and expr.op.dd_out == self.prev_dd
@@ -420,14 +420,14 @@ class DistributedMapper(CSECachingMapperMixin, IdentityMapper):
         from meshmode.mesh import BTAG_PARTITION
         from meshmode.discretization.connection import (FACE_RESTR_ALL,
                                                         FACE_RESTR_INTERIOR)
-        if (isinstance(expr.op, op.InterpolationOperator)
+        if (isinstance(expr.op, op.ProjectionOperator)
                 and expr.op.dd_in.domain_tag is FACE_RESTR_INTERIOR
                 and expr.op.dd_out.domain_tag is FACE_RESTR_ALL):
             distributed_work = 0
             for i_remote_part in self.connected_parts:
                 mapped_field = RankGeometryChanger(i_remote_part)(expr.field)
                 btag_part = BTAG_PARTITION(i_remote_part)
-                distributed_work += op.InterpolationOperator(dd_in=btag_part,
+                distributed_work += op.ProjectionOperator(dd_in=btag_part,
                                              dd_out=expr.op.dd_out)(mapped_field)
             return expr + distributed_work
         else:
@@ -458,9 +458,9 @@ class RankGeometryChanger(CSECachingMapperMixin, IdentityMapper):
                     dd_in=self.new_dd,
                     dd_out=self.new_dd,
                     unique_id=expr.op.unique_id)(field)
-        elif (isinstance(expr.op, op.InterpolationOperator)
+        elif (isinstance(expr.op, op.ProjectionOperator)
                     and expr.op.dd_out == self.prev_dd):
-            return op.InterpolationOperator(dd_in=expr.op.dd_in,
+            return op.ProjectionOperator(dd_in=expr.op.dd_in,
                                             dd_out=self.new_dd)(expr.field)
         elif (isinstance(expr.op, op.RefDiffOperator)
                     and expr.op.dd_out == self.prev_dd
@@ -824,8 +824,8 @@ class StringifyMapper(pymbolic.mapper.stringifier.StringifyMapper):
     def map_function_symbol(self, expr, enclosing_prec):
         return expr.name
 
-    def map_interpolation(self, expr, enclosing_prec):
-        return "Interp" + self._format_op_dd(expr)
+    def map_projection(self, expr, enclosing_prec):
+        return "Project" + self._format_op_dd(expr)
 
 
 class PrettyStringifyMapper(
@@ -882,14 +882,14 @@ class QuadratureCheckerAndRemover(CSECachingMapperMixin, IdentityMapper):
         import grudge.symbolic.operators as op
         # changed
 
-        if dd_in == dd_out and isinstance(expr.op, op.InterpolationOperator):
+        if dd_in == dd_out and isinstance(expr.op, op.ProjectionOperator):
             # This used to be to-quad interpolation and has become a no-op.
             # Remove it.
             return self.rec(expr.field)
 
         if isinstance(expr.op, op.StiffnessTOperator):
             new_op = type(expr.op)(expr.op.xyz_axis, dd_in, dd_out)
-        elif isinstance(expr.op, (op.FaceMassOperator, op.InterpolationOperator)):
+        elif isinstance(expr.op, (op.FaceMassOperator, op.ProjectionOperator)):
             new_op = type(expr.op)(dd_in, dd_out)
         else:
             raise NotImplementedError("do not know how to modify dd_in and dd_out "
@@ -952,7 +952,7 @@ class EmptyFluxKiller(CSECachingMapperMixin, IdentityMapper):
 
     def map_operator_binding(self, expr):
         from meshmode.mesh import is_boundary_tag_empty
-        if (isinstance(expr.op, sym.InterpolationOperator)
+        if (isinstance(expr.op, sym.ProjectionOperator)
                 and expr.op.dd_out.is_boundary_or_partition_interface()):
             domain_tag = expr.op.dd_out.domain_tag
             assert isinstance(domain_tag, sym.DTAG_BOUNDARY)
diff --git a/grudge/symbolic/operators.py b/grudge/symbolic/operators.py
index 405db3e1b044e1438c0ef9b8799fcd9dd919bb55..b5d240606136a31764cfab68626e1fc78f401a8f 100644
--- a/grudge/symbolic/operators.py
+++ b/grudge/symbolic/operators.py
@@ -38,9 +38,9 @@ Basic Operators
 
 .. autoclass:: Operator
 .. autoclass:: ElementwiseLinearOperator
-.. autoclass:: InterpolationOperator
+.. autoclass:: ProjectionOperator
 
-.. data:: interp
+.. data:: project
 
 Reductions
 ^^^^^^^^^^
@@ -148,17 +148,17 @@ class ElementwiseLinearOperator(Operator):
     mapper_method = intern("map_elementwise_linear")
 
 
-class InterpolationOperator(Operator):
+class ProjectionOperator(Operator):
     def __init__(self, dd_in, dd_out):
-        super(InterpolationOperator, self).__init__(dd_in, dd_out)
+        super(ProjectionOperator, self).__init__(dd_in, dd_out)
 
     def __call__(self, expr):
         from pytools.obj_array import obj_array_vectorize
 
-        def interp_one(subexpr):
+        def project_one(subexpr):
             from pymbolic.primitives import is_constant
             if self.dd_in == self.dd_out:
-                # no-op interpolation, go away
+                # no-op projection, go away
                 return subexpr
             elif is_constant(subexpr):
                 return subexpr
@@ -166,12 +166,30 @@ class InterpolationOperator(Operator):
                 from grudge.symbolic.primitives import OperatorBinding
                 return OperatorBinding(self, subexpr)
 
-        return obj_array_vectorize(interp_one, expr)
+        return obj_array_vectorize(project_one, expr)
+
+    mapper_method = intern("map_projection")
+
+
+project = ProjectionOperator
+
+
+class InterpolationOperator(ProjectionOperator):
+    def __init__(self, dd_in, dd_out):
+        from warnings import warn
+        warn("'InterpolationOperator' is deprecated, "
+                "use 'ProjectionOperator' instead.",
+                DeprecationWarning, stacklevel=2)
+
+        super(InterpolationOperator, self).__init__(dd_in, dd_out)
 
-    mapper_method = intern("map_interpolation")
 
+def interp(dd_in, dd_out):
+    from warnings import warn
+    warn("using 'interp' is deprecated, use 'project' instead.",
+            DeprecationWarning, stacklevel=2)
 
-interp = InterpolationOperator
+    return ProjectionOperator(dd_in, dd_out)
 
 
 # {{{ element reduction: sum, min, max
diff --git a/grudge/symbolic/primitives.py b/grudge/symbolic/primitives.py
index e6c466c7b2ee95c2a05810c8b8f64384b9e41441..ef6ce5a7d38f92b3693b8132c305a500f7a8954f 100644
--- a/grudge/symbolic/primitives.py
+++ b/grudge/symbolic/primitives.py
@@ -512,8 +512,8 @@ def forward_metric_derivative(xyz_axis, rst_axis, dd=None):
     result = diff_op(NodeCoordinateComponent(xyz_axis, inner_dd))
 
     if dd.uses_quadrature():
-        from grudge.symbolic.operators import interp
-        result = interp(inner_dd, dd)(result)
+        from grudge.symbolic.operators import project
+        result = project(inner_dd, dd)(result)
 
     return cse(
             result,
@@ -648,15 +648,15 @@ def mv_normal(dd, ambient_dim, dim=None):
     if dim == 0:
         # NOTE: when the mesh is 0D, we do not have a clear notion of
         # `exterior normal`, so we just take the tangent of the 1D element,
-        # interpolate it to the element faces and make it signed
+        # project it to the element faces and make it signed
         tangent = parametrization_derivative(
                 ambient_dim, dim=1, dd=DD_VOLUME)
         tangent = tangent / sqrt(tangent.norm_squared())
 
-        from grudge.symbolic.operators import interp
-        interp = interp(DD_VOLUME, dd)
+        from grudge.symbolic.operators import project
+        project = project(DD_VOLUME, dd)
         mv = MultiVector(np.array([
-            mv.as_scalar() * interp(t) for t in tangent.as_vector()
+            mv.as_scalar() * project(t) for t in tangent.as_vector()
             ]))
 
     return cse(mv, "normal", cse_scope.DISCRETIZATION)
@@ -720,15 +720,15 @@ class TracePair:
 
 
 def int_tpair(expression, qtag=None):
-    from grudge.symbolic.operators import interp, OppositeInteriorFaceSwap
+    from grudge.symbolic.operators import project, OppositeInteriorFaceSwap
 
-    i = interp("vol", "int_faces")(expression)
+    i = project("vol", "int_faces")(expression)
     e = cse(OppositeInteriorFaceSwap()(i))
 
     if qtag is not None and qtag != QTAG_NONE:
         q_dd = DOFDesc("int_faces", qtag)
-        i = cse(interp("int_faces", q_dd)(i))
-        e = cse(interp("int_faces", q_dd)(e))
+        i = cse(project("int_faces", q_dd)(i))
+        e = cse(project("int_faces", q_dd)(e))
     else:
         q_dd = "int_faces"
 
@@ -756,8 +756,8 @@ def bv_tpair(dd, interior, exterior):
         representing the exterior value to be used
         for the flux.
     """
-    from grudge.symbolic.operators import interp
-    interior = cse(interp("vol", dd)(interior))
+    from grudge.symbolic.operators import project
+    interior = cse(project("vol", dd)(interior))
     return TracePair(dd, interior, exterior)
 
 # }}}
diff --git a/test/test_grudge.py b/test/test_grudge.py
index 472c9ccf5b5a2d827a84a2c07d5b07cdfa3f0421..c68a71b6bd353a29ac0fc5829cc9330eac3ae866 100644
--- a/test/test_grudge.py
+++ b/test/test_grudge.py
@@ -235,7 +235,7 @@ def test_2d_gauss_theorem(ctx_factory):
                 ).sum())
             -  # noqa: W504
             sym.integral(
-                sym.interp("vol", sym.BTAG_ALL)(f(sym.nodes(2)))
+                sym.project("vol", sym.BTAG_ALL)(f(sym.nodes(2)))
                 .dot(sym.normal(sym.BTAG_ALL, 2)),
                 dd=sym.BTAG_ALL)
             )(actx)
diff --git a/test/test_mpi_communication.py b/test/test_mpi_communication.py
index c4776ce2d0d2498c026f4a10ff7c45a0590f3853..a5109737106279f0a76d147a9d77a4e35d2d6d84 100644
--- a/test/test_mpi_communication.py
+++ b/test/test_mpi_communication.py
@@ -75,15 +75,15 @@ def simple_mpi_communication_entrypoint():
     myfunc = bind(vol_discr, myfunc_symb)(actx)
 
     sym_all_faces_func = sym.cse(
-        sym.interp("vol", "all_faces")(sym.var("myfunc")))
+        sym.project("vol", "all_faces")(sym.var("myfunc")))
     sym_int_faces_func = sym.cse(
-        sym.interp("vol", "int_faces")(sym.var("myfunc")))
+        sym.project("vol", "int_faces")(sym.var("myfunc")))
     sym_bdry_faces_func = sym.cse(
-        sym.interp(sym.BTAG_ALL, "all_faces")(
-            sym.interp("vol", sym.BTAG_ALL)(sym.var("myfunc"))))
+        sym.project(sym.BTAG_ALL, "all_faces")(
+            sym.project("vol", sym.BTAG_ALL)(sym.var("myfunc"))))
 
     bound_face_swap = bind(vol_discr,
-        sym.interp("int_faces", "all_faces")(
+        sym.project("int_faces", "all_faces")(
             sym.OppositeInteriorFaceSwap("int_faces")(
                 sym_int_faces_func)
             ) - (sym_all_faces_func - sym_bdry_faces_func)