diff --git a/hog/forms.py b/hog/forms.py
index 8e09528af483f950ea7e40c73248586a6b51d47a..ed494e59035d60cd814ee6d0bb413d5913e081df 100644
--- a/hog/forms.py
+++ b/hog/forms.py
@@ -80,6 +80,10 @@ Weak formulation
     v: test function  (space: {test})
     
     ∫ ∇u : ∇v
+    
+    Note that the double contraction (:) reduces to the dot product for scalar function spaces, i.e. the form becomes
+    
+    ∫ ∇u · ∇v 
 """
 
     if trial != test:
@@ -90,7 +94,6 @@ Weak formulation
     with TimedLogger("assembling diffusion matrix", level=logging.DEBUG):
         tabulation = Tabulation(symbolizer)
 
-        jac_affine = symbolizer.jac_ref_to_affine(geometry.dimensions)
         jac_affine_inv = symbolizer.jac_ref_to_affine_inv(geometry.dimensions)
         jac_affine_det = symbolizer.abs_det_jac_ref_to_affine()
 
diff --git a/hog/operator_generation/function_space_impls.py b/hog/operator_generation/function_space_impls.py
index 276b3622d70ccf7f1b581c0ee0532bdb3944b46d..130539f6040a59defd1ed11b668adcffa775ed5f 100644
--- a/hog/operator_generation/function_space_impls.py
+++ b/hog/operator_generation/function_space_impls.py
@@ -104,9 +104,11 @@ class FunctionSpaceImpl(ABC):
             if isinstance(func_space.component_function_space, LagrangianFunctionSpace):
                 if func_space.component_function_space.degree == 1:
                     impl_class = P1VectorFunctionSpaceImpl
+                elif func_space.component_function_space.degree == 2:
+                    impl_class = P2VectorFunctionSpaceImpl
                 else:
                     raise HOGException(
-                        "TensorialVectorFunctionSpaces are only supported for P1 components."
+                        "TensorialVectorFunctionSpaces not supported for the chosen components."
                     )
             else:
                 raise HOGException(
@@ -468,7 +470,7 @@ class P1VectorFunctionSpaceImpl(FunctionSpaceImpl):
         is_pointer: bool = False,
     ):
         super().__init__(fe_space, name, type_descriptor, is_pointer)
-        self.fields: Dict = {}
+        self.fields: Dict[int, Field] = {}
 
     def _field_name(self, component: int) -> str:
         return self.name + f"_{component}"
@@ -633,6 +635,187 @@ class P1VectorFunctionSpaceImpl(FunctionSpaceImpl):
         )
 
 
+class P2VectorFunctionSpaceImpl(FunctionSpaceImpl):
+    def __init__(
+        self,
+        fe_space: FunctionSpace,
+        name: str,
+        type_descriptor: HOGType,
+        is_pointer: bool = False,
+    ):
+        super().__init__(fe_space, name, type_descriptor, is_pointer)
+        self.fields: Dict[Tuple[str, int], Field] = {}
+
+    def _field_name(self, component: int, dof_type: str) -> str:
+        """dof_type should either be 'vertex' or 'edge'"""
+        return self.name + f"_{dof_type}_{component}"
+
+    def _raw_pointer_name(self, component: int, dof_type: str) -> str:
+        """dof_type should either be 'vertex' or 'edge'"""
+        return f"_data_" + self._field_name(component, dof_type)
+
+    def pre_communication(self, dim: int) -> str:
+        if dim == 2:
+            return f"communication::syncVectorFunctionBetweenPrimitives( {self.name}, level, communication::syncDirection_t::LOW2HIGH );"
+        else:
+            ret_str = ""
+            for i in range(dim):
+                ret_str += (
+                    f"{self._deref()}[{i}].communicate< Face, Cell >( level );\n"
+                    f"{self._deref()}[{i}].communicate< Edge, Cell >( level );\n"
+                    f"{self._deref()}[{i}].communicate< Vertex, Cell >( level );"
+                )
+            return ret_str
+
+    def zero_halos(self, dim: int) -> str:
+        if dim == 2:
+            return (
+                f"for ( const auto& idx : vertexdof::macroface::Iterator( level ) )\n"
+                f"{{\n"
+                f"    if ( vertexdof::macroface::isVertexOnBoundary( level, idx ) )\n"
+                f"    {{\n"
+                f"        auto arrayIdx = vertexdof::macroface::index( level, idx.x(), idx.y() );\n"
+                f"        {self._raw_pointer_name(0, 'vertex')}[arrayIdx] = walberla::numeric_cast< {self.type_descriptor.pystencils_type} >( 0 );\n"
+                f"        {self._raw_pointer_name(1, 'vertex')}[arrayIdx] = walberla::numeric_cast< {self.type_descriptor.pystencils_type} >( 0 );\n"
+                f"    }}\n"
+                f"}}\n"
+                f"for ( const auto& idx : edgedof::macroface::Iterator( level ) )\n"
+                f"{{\n"
+                f"    for ( const auto& orientation : edgedof::faceLocalEdgeDoFOrientations )\n"
+                f"    {{\n"
+                f"        if ( !edgedof::macroface::isInnerEdgeDoF( level, idx, orientation ) )\n"
+                f"        {{\n"
+                f"            auto arrayIdx = edgedof::macroface::index( level, idx.x(), idx.y(), orientation );\n"
+                f"            {self._raw_pointer_name(0, 'edge')}[arrayIdx] = walberla::numeric_cast< {self.type_descriptor.pystencils_type} >( 0 );\n"
+                f"            {self._raw_pointer_name(1, 'edge')}[arrayIdx] = walberla::numeric_cast< {self.type_descriptor.pystencils_type} >( 0 );\n"
+                f"        }}\n"
+                f"    }}\n"
+                f"}}"
+            )
+        else:
+            return (
+                f"for ( const auto& idx : vertexdof::macrocell::Iterator( level ) )\n"
+                f"{{\n"
+                f"if ( !vertexdof::macrocell::isOnCellFace( idx, level ).empty() )\n"
+                f"    {{\n"
+                f"        auto arrayIdx = vertexdof::macrocell::index( level, idx.x(), idx.y(), idx.z() );\n"
+                f"        {self._raw_pointer_name(0, 'vertex')}[arrayIdx] = {self.type_descriptor.pystencils_type}( 0 );\n"
+                f"        {self._raw_pointer_name(1, 'vertex')}[arrayIdx] = {self.type_descriptor.pystencils_type}( 0 );\n"
+                f"        {self._raw_pointer_name(2, 'vertex')}[arrayIdx] = {self.type_descriptor.pystencils_type}( 0 );\n"
+                f"    }}\n"
+                f"}}\n"
+                f"edgedof::macrocell::setBoundaryToZero( level, cell, {self._deref()}[0].getEdgeDoFFunction().getCellDataID() );\n"
+                f"edgedof::macrocell::setBoundaryToZero( level, cell, {self._deref()}[1].getEdgeDoFFunction().getCellDataID() );\n"
+                f"edgedof::macrocell::setBoundaryToZero( level, cell, {self._deref()}[2].getEdgeDoFFunction().getCellDataID() );\n"
+            )
+
+    def post_communication(
+        self, dim: int, params: str, transform_basis: bool = True
+    ) -> str:
+        if dim == 2:
+            ret_str = ""
+            for i in range(dim):
+                ret_str += (
+                    f"{self._deref()}[{i}].getVertexDoFFunction().communicateAdditively< Face, Edge >( {params} );\n"
+                    f"{self._deref()}[{i}].getVertexDoFFunction().communicateAdditively< Face, Vertex >( {params} );\n"
+                    f"{self._deref()}[{i}].getEdgeDoFFunction().communicateAdditively< Face, Edge >( {params} );"
+                )
+            return ret_str
+        else:
+            ret_str = ""
+            for i in range(dim):
+                ret_str += (
+                    f"{self._deref()}[{i}].getVertexDoFFunction().communicateAdditively< Cell, Face >( {params} );\n"
+                    f"{self._deref()}[{i}].getVertexDoFFunction().communicateAdditively< Cell, Edge >( {params} );\n"
+                    f"{self._deref()}[{i}].getVertexDoFFunction().communicateAdditively< Cell, Vertex >( {params} );\n"
+                    f"{self._deref()}[{i}].getEdgeDoFFunction().communicateAdditively< Cell, Face >( {params} );\n"
+                    f"{self._deref()}[{i}].getEdgeDoFFunction().communicateAdditively< Cell, Edge >( {params} );"
+                )
+            return ret_str
+
+    def pointer_retrieval(self, dim: int) -> str:
+        """C++ code for retrieving pointers to the numerical data stored in the macro primitives `face` (2d) or `cell` (3d)."""
+        Macro = {2: "Face", 3: "Cell"}[dim]
+        macro = {2: "face", 3: "cell"}[dim]
+
+        ret_str = ""
+        for i in range(dim):
+            ret_str += f"{self.type_descriptor.pystencils_type}* {self._raw_pointer_name(i, 'vertex')} = {macro}.getData( {self._deref()}[{i}].getVertexDoFFunction().get{Macro}DataID() )->getPointer( level );\n"
+            ret_str += f"{self.type_descriptor.pystencils_type}* {self._raw_pointer_name(i, 'edge')} = {macro}.getData( {self._deref()}[{i}].getEdgeDoFFunction().get{Macro}DataID() )->getPointer( level );\n"
+        return ret_str
+
+    def invert_elementwise(self, dim: int) -> str:
+        ret_str = ""
+        for i in range(dim):
+            ret_str += f"{self._deref()}[{i}].invertElementwise( level );\n"
+        return ret_str
+
+    def local_dofs(
+        self,
+        geometry: ElementGeometry,
+        element_index: Tuple[int, int, int],
+        element_type: Union[FaceType, CellType],
+        indexing_info: IndexingInfo,
+    ) -> List[Field.Access]:
+        """
+        Returns the element-local DoFs (field accesses) in a list (i.e., linearized).
+
+        See P1VectorFunctionSpaceImpl::local_dofs() for details.
+        """
+
+        for dt in ["vertex", "edge"]:
+            for c in range(geometry.dimensions):
+                if (dt, c) not in self.fields:
+                    self.fields[(dt, c)] = self._create_field(self._field_name(c, dt))
+
+        vertex_dof_indices = micro_element_to_vertex_indices(
+            geometry, element_type, element_index
+        )
+
+        edge_dof_indices = micro_vertex_to_edge_indices(geometry, vertex_dof_indices)
+
+        vertex_array_indices = [
+            dof_idx.array_index(geometry, indexing_info)
+            for dof_idx in vertex_dof_indices
+        ]
+
+        edge_array_indices = [
+            dof_idx.array_index(geometry, indexing_info) for dof_idx in edge_dof_indices
+        ]
+
+        loc_dofs = []
+
+        for c in range(geometry.dimensions):
+            loc_dofs += [
+                self.fields[("vertex", c)].absolute_access((idx,), (0,))
+                for idx in vertex_array_indices
+            ]
+            loc_dofs += [
+                self.fields[("edge", c)].absolute_access((idx,), (0,))
+                for idx in edge_array_indices
+            ]
+
+        return loc_dofs
+
+    def func_type_string(self) -> str:
+        return f"P2VectorFunction< {self.type_descriptor.pystencils_type} >"
+
+    def includes(self) -> Set[str]:
+        return {f"hyteg/p2functionspace/P2VectorFunction.hpp",
+                f"hyteg/p2functionspace/P2Function.hpp"}
+
+    def dof_transformation(
+        self,
+        geometry: ElementGeometry,
+        element_index: Tuple[int, int, int],
+        element_type: Union[FaceType, CellType],
+    ) -> Tuple[CustomCodeNode, sp.MatrixBase]:
+        return (
+            CustomCodeNode("", [], []),
+            sp.Identity(self.fe_space.num_dofs(geometry)),
+        )
+
+
 class N1E1FunctionSpaceImpl(FunctionSpaceImpl):
     def __init__(
         self,