diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index ea10c572e03cae4daeb1087517576b0fbedd0139..af88cbb30257a23b209e67f8fbd96bef6ab5c8d5 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -10,6 +10,14 @@ Python 2.7:
   except:
   - tags
 
+Python 2.7 Conda:
+  script:
+  - CONDA_ENVIRONMENT=.test-py2.yml
+  - curl -L -O -k https://gitlab.tiker.net/inducer/ci-support/raw/master/build-and-test-py-project-within-miniconda.sh
+  - ". ./build-and-test-py-project-within-miniconda.sh"
+  except:
+  - tags
+
 Python 3.5:
   script:
   - py_version=3.5
@@ -22,6 +30,14 @@ Python 3.5:
   except:
   - tags
 
+Python 3.5 Conda:
+  script:
+  - CONDA_ENVIRONMENT=.test-py3.yml
+  - curl -L -O -k https://gitlab.tiker.net/inducer/ci-support/raw/master/build-and-test-py-project-within-miniconda.sh
+  - ". ./build-and-test-py-project-within-miniconda.sh"
+  except:
+  - tags
+
 Python 2.6:
   script:
   - py_version=2.6
diff --git a/.test-py2.yml b/.test-py2.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f21c307c51bdfdc92e0192cd5f60a887a1d3454d
--- /dev/null
+++ b/.test-py2.yml
@@ -0,0 +1,12 @@
+name: py2
+channels:
+ - symengine/label/dev
+ - conda-forge
+ - defaults
+dependencies:
+ - pexpect
+ - pytools
+ - conda-forge::numpy
+ - conda-forge::sympy
+ - python=2.7
+ - symengine/label/dev::python-symengine=0.2.0.53.g83912b7=py27_1
diff --git a/.test-py3.yml b/.test-py3.yml
new file mode 100644
index 0000000000000000000000000000000000000000..d80032887699f58f8709e50a40110298c9ccd5c6
--- /dev/null
+++ b/.test-py3.yml
@@ -0,0 +1,11 @@
+name: py3
+channels:
+ - symengine/label/dev
+ - conda-forge
+ - defaults
+dependencies:
+ - pexpect
+ - conda-forge::numpy
+ - conda-forge::sympy
+ - python=3.5
+ - symengine/label/dev::python-symengine=0.2.0.53.g83912b7=py35_1
diff --git a/pymbolic/interop/common.py b/pymbolic/interop/common.py
new file mode 100644
index 0000000000000000000000000000000000000000..256dda7c34565002a99ce3d3235a07e2a78a9b52
--- /dev/null
+++ b/pymbolic/interop/common.py
@@ -0,0 +1,164 @@
+from __future__ import division, absolute_import
+
+__copyright__ = "Copyright (C) 2009-2013 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.
+"""
+
+import pymbolic.primitives as prim
+from pymbolic.mapper.evaluator import EvaluationMapper
+
+
+class SympyLikeMapperBase(object):
+
+    def __call__(self, expr, *args, **kwargs):
+        return self.rec(expr, *args, **kwargs)
+
+    def rec(self, expr, *args, **kwargs):
+        mro = list(type(expr).__mro__)
+        dispatch_class = kwargs.pop("dispatch_class", type(self))
+
+        while mro:
+            method_name = "map_"+mro.pop(0).__name__
+
+            try:
+                method = getattr(dispatch_class, method_name)
+            except AttributeError:
+                pass
+            else:
+                return method(self, expr, *args, **kwargs)
+
+        return self.not_supported(expr)
+
+    def not_supported(self, expr):
+        print(expr, expr.__class__.__mro__)
+        raise NotImplementedError(
+                "%s does not know how to map type '%s'"
+                % (type(self).__name__,
+                    type(expr).__name__))
+
+
+# {{{ sympy like -> pymbolic
+
+class SympyLikeToPymbolicMapper(SympyLikeMapperBase):
+
+    # {{{ utils
+
+    def to_float(self, expr):
+        return float(expr)
+
+    def function_name(self, expr):
+        # Given a symbolic function application f(x), return the name of f as a
+        # string
+        raise NotImplementedError()
+
+    # }}}
+
+    def map_Symbol(self, expr):  # noqa
+        return prim.Variable(str(expr.name))
+
+    def map_Rational(self, expr):  # noqa
+        p, q = expr.p, expr.q
+
+        num = self.rec(p)
+        denom = self.rec(q)
+
+        if prim.is_zero(denom-1):
+            return num
+        return prim.Quotient(num, denom)
+
+    def map_Integer(self, expr):  # noqa
+        return int(expr)
+
+    def map_Add(self, expr):  # noqa
+        return prim.Sum(tuple(self.rec(arg) for arg in expr.args))
+
+    def map_Mul(self, expr):  # noqa
+        return prim.Product(tuple(self.rec(arg) for arg in expr.args))
+
+    def map_Pow(self, expr):  # noqa
+        base, exp = expr.args
+        return prim.Power(self.rec(base), self.rec(exp))
+
+    def map_Subs(self, expr):  # noqa
+        return prim.Substitution(self.rec(expr.expr),
+                tuple(v.name for v in expr.variables),
+                tuple(self.rec(v) for v in expr.point),
+                )
+
+    def map_Derivative(self, expr):  # noqa
+        return prim.Derivative(self.rec(expr.expr),
+                tuple(v.name for v in expr.variables))
+
+    def map_CSE(self, expr):  # noqa
+        return prim.CommonSubexpression(
+                self.rec(expr.args[0]), expr.prefix)
+
+    def not_supported(self, expr):
+        if isinstance(expr, int):
+            return expr
+        elif getattr(expr, "is_Function", False):
+            return prim.Variable(self.function_name(expr))(
+                    *tuple(self.rec(arg) for arg in expr.args))
+        else:
+            return SympyLikeMapperBase.not_supported(self, expr)
+
+# }}}
+
+
+# {{{ pymbolic -> sympy
+
+class PymbolicToSympyLikeMapper(EvaluationMapper):
+
+    def map_variable(self, expr):
+        return self.sym.Symbol(expr.name)
+
+    def map_constant(self, expr):
+        return self.sym.sympify(expr)
+
+    def map_call(self, expr):
+        if isinstance(expr.function, prim.Variable):
+            func_name = expr.function.name
+            try:
+                func = getattr(self.sym.functions, func_name)
+            except AttributeError:
+                func = self.sym.Function(func_name)
+            return func(*[self.rec(par) for par in expr.parameters])
+        else:
+            self.raise_conversion_error(expr)
+
+    def map_subscript(self, expr):
+        if isinstance(expr.aggregate, prim.Variable) and isinstance(expr.index, int):
+            return self.sym.Symbol("%s_%d" % (expr.aggregate.name, expr.index))
+        else:
+            self.raise_conversion_error(expr)
+
+    def map_substitution(self, expr):
+        return self.sym.Subs(self.rec(expr.child),
+                tuple(self.sym.Symbol(v) for v in expr.variables),
+                tuple(self.rec(v) for v in expr.values),
+                )
+
+    def map_derivative(self, expr):
+        raise NotImplementedError()
+
+# }}}
+
+# vim: fdm=marker
diff --git a/pymbolic/interop/symengine.py b/pymbolic/interop/symengine.py
new file mode 100644
index 0000000000000000000000000000000000000000..ab68e7fde1596e390ad59e55b40827589d9c6f80
--- /dev/null
+++ b/pymbolic/interop/symengine.py
@@ -0,0 +1,99 @@
+from __future__ import division, absolute_import
+
+__copyright__ = """
+Copyright (C) 2017 Matt Wala
+"""
+
+__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 pymbolic.interop.common import (
+    SympyLikeToPymbolicMapper, PymbolicToSympyLikeMapper)
+
+import pymbolic.primitives as prim
+import symengine.sympy_compat
+
+
+__doc__ = """
+.. class:: SymEngineToPymbolicMapper
+
+    .. method:: __call__(expr)
+
+.. class:: PymbolicToSymEngineMapper
+
+    .. method:: __call__(expr)
+"""
+
+
+# {{{ symengine -> pymbolic
+
+class SymEngineToPymbolicMapper(SympyLikeToPymbolicMapper):
+
+    def map_Pow(self, expr):  # noqa
+        # SymEngine likes to use as e**a to express exp(a); we undo that here.
+        base, exp = expr.args
+        if base == symengine.E:
+            return prim.Variable("exp")(self.rec(exp))
+        else:
+            return prim.Power(self.rec(base), self.rec(exp))
+
+    def map_Constant(self, expr):  # noqa
+        return self.rec(expr.n())
+
+    map_Complex = map_Constant
+
+    def map_ComplexDouble(self, expr):  # noqa
+        r = self.rec(expr.real_part())
+        i = self.rec(expr.imaginary_part())
+        if prim.is_zero(i):
+            return r
+        else:
+            return r + 1j * i
+
+    map_RealDouble = SympyLikeToPymbolicMapper.to_float
+
+    def function_name(self, expr):
+        try:
+            # For FunctionSymbol instances
+            return expr.get_name()
+        except AttributeError:
+            # For builtin functions
+            return type(expr).__name__
+
+# }}}
+
+
+# {{{ pymbolic -> symengine
+
+class PymbolicToSymEngineMapper(PymbolicToSympyLikeMapper):
+
+    sym = symengine.sympy_compat
+
+    def raise_conversion_error(self, expr):
+        raise RuntimeError(
+            "do not know how to translate '%s' to symengine" % expr)
+
+    def map_derivative(self, expr):
+        return self.sym.Derivative(self.rec(expr.child),
+                [self.sym.Symbol(v) for v in expr.variables])
+
+# }}}
+
+# vim: fdm=marker
diff --git a/pymbolic/interop/sympy.py b/pymbolic/interop/sympy.py
index 034fb9c5aea314998ff33fb9e17c219c4b11591e..7e1f4399a6ce0bf21c5931f084e1b1aef6d7c3fc 100644
--- a/pymbolic/interop/sympy.py
+++ b/pymbolic/interop/sympy.py
@@ -1,6 +1,9 @@
 from __future__ import division, absolute_import
 
-__copyright__ = "Copyright (C) 2009-2013 Andreas Kloeckner"
+__copyright__ = """
+Copyright (C) 2017 Matt Wala
+Copyright (C) 2009-2013 Andreas Kloeckner
+"""
 
 __license__ = """
 Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -22,9 +25,11 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 THE SOFTWARE.
 """
 
-import pymbolic.primitives as prim
-from pymbolic.mapper.evaluator import EvaluationMapper
-import sympy as sp
+from pymbolic.interop.common import (
+    SympyLikeToPymbolicMapper, PymbolicToSympyLikeMapper)
+
+import sympy
+
 
 __doc__ = """
 .. class:: SympyToPymbolicMapper
@@ -37,145 +42,53 @@ __doc__ = """
 """
 
 
-class SympyMapper(object):
-    def __call__(self, expr, *args, **kwargs):
-        return self.rec(expr, *args, **kwargs)
-
-    def rec(self, expr, *args, **kwargs):
-        mro = list(type(expr).__mro__)
-        dispatch_class = kwargs.pop("dispatch_class", type(self))
-
-        while mro:
-            method_name = "map_"+mro.pop(0).__name__
+# {{{ symengine -> pymbolic
 
-            try:
-                method = getattr(dispatch_class, method_name)
-            except AttributeError:
-                pass
-            else:
-                return method(self, expr, *args, **kwargs)
-
-        return self.not_supported(expr)
-
-    def not_supported(self, expr):
-        raise NotImplementedError(
-                "%s does not know how to map type '%s'"
-                % (type(self).__name__,
-                    type(expr).__name__))
-
-
-class CSE(sp.Function):
-    """A function to translate to a Pymbolic CSE."""
-
-    nargs = 1
-
-
-def make_cse(arg, prefix=None):
-    result = CSE(arg)
-    result.prefix = prefix
-    return result
+class SympyToPymbolicMapper(SympyLikeToPymbolicMapper):
 
+    def map_ImaginaryUnit(self, expr):  # noqa
+        return 1j
 
-# {{{ sympy -> pymbolic
+    map_Float = SympyLikeToPymbolicMapper.to_float
 
-class SympyToPymbolicMapper(SympyMapper):
-    def map_Symbol(self, expr):  # noqa
-        return prim.Variable(expr.name)
+    map_NumberSymbol = SympyLikeToPymbolicMapper.to_float
 
-    def map_ImaginaryUnit(self, expr):  # noqa
-        return 1j
+    def function_name(self, expr):
+        return type(expr).__name__
 
     # only called for Py2
     def map_long(self, expr):
         return long(expr)  # noqa
 
-    def map_Float(self, expr):  # noqa
-        return float(expr)
-
-    def map_Pi(self, expr):  # noqa
-        return float(expr)
-
-    def map_Add(self, expr):  # noqa
-        return prim.Sum(tuple(self.rec(arg) for arg in expr.args))
-
-    def map_Mul(self, expr):  # noqa
-        return prim.Product(tuple(self.rec(arg) for arg in expr.args))
-
-    def map_Rational(self, expr):  # noqa
-        num = self.rec(expr.p)
-        denom = self.rec(expr.q)
+# }}}
 
-        if prim.is_zero(denom-1):
-            return num
-        return prim.Quotient(num, denom)
 
-    def map_Pow(self, expr):  # noqa
-        return prim.Power(self.rec(expr.base), self.rec(expr.exp))
+# {{{ pymbolic -> symengine
 
-    def map_Subs(self, expr):  # noqa
-        return prim.Substitution(self.rec(expr.expr),
-                tuple(v.name for v in expr.variables),
-                tuple(self.rec(v) for v in expr.point),
-                )
+class PymbolicToSympyMapper(PymbolicToSympyLikeMapper):
 
-    def map_Derivative(self, expr):  # noqa
-        return prim.Derivative(self.rec(expr.expr),
-                tuple(v.name for v in expr.variables))
+    sym = sympy
 
-    def map_CSE(self, expr):  # noqa
-        return prim.CommonSubexpression(
-                self.rec(expr.args[0]), expr.prefix)
+    def raise_conversion_error(self, expr):
+        raise RuntimeError(
+            "do not know how to translate '%s' to sympy" % expr)
 
-    def not_supported(self, expr):
-        if isinstance(expr, int):
-            return expr
-        elif getattr(expr, "is_Function", False):
-            return prim.Variable(type(expr).__name__)(
-                    *tuple(self.rec(arg) for arg in expr.args))
-        else:
-            return SympyMapper.not_supported(self, expr)
+    def map_derivative(self, expr):
+        return self.sym.Derivative(self.rec(expr.child),
+                *[self.sym.Symbol(v) for v in expr.variables])
 
 # }}}
 
 
-# {{{ pymbolic -> sympy
-
-class PymbolicToSympyMapper(EvaluationMapper):
-    def map_variable(self, expr):
-        return sp.Symbol(expr.name)
-
-    def map_constant(self, expr):
-        return sp.sympify(expr)
-
-    def map_call(self, expr):
-        if isinstance(expr.function, prim.Variable):
-            func_name = expr.function.name
-            try:
-                func = getattr(sp.functions, func_name)
-            except AttributeError:
-                func = sp.Function(func_name)
-            return func(*[self.rec(par) for par in expr.parameters])
-        else:
-            raise RuntimeError("do not know how to translate '%s' to sympy"
-                    % expr)
-
-    def map_subscript(self, expr):
-        if isinstance(expr.aggregate, prim.Variable) and isinstance(expr.index, int):
-            return sp.Symbol("%s_%d" % (expr.aggregate.name, expr.index))
-        else:
-            raise RuntimeError("do not know how to translate '%s' to sympy"
-                    % expr)
-
-    def map_substitution(self, expr):
-        return sp.Subs(self.rec(expr.child),
-                tuple(sp.Symbol(v) for v in expr.variables),
-                tuple(self.rec(v) for v in expr.values),
-                )
+class CSE(sympy.Function):
+    """A function to translate to a Pymbolic CSE."""
+    nargs = 1
 
-    def map_derivative(self, expr):
-        return sp.Derivative(self.rec(expr.child),
-                *[sp.Symbol(v) for v in expr.variables])
 
-# }}}
+def make_cse(arg, prefix=None):
+    result = CSE(arg)
+    result.prefix = prefix
+    return result
+
 
 # vim: fdm=marker
diff --git a/test/test_pymbolic.py b/test/test_pymbolic.py
index 95182c741f8479884d7b9af9b5d70bf9bd6783be..fe7c8a395b1ad433395d9e0771f6b833ca74884f 100644
--- a/test/test_pymbolic.py
+++ b/test/test_pymbolic.py
@@ -481,8 +481,8 @@ def test_unifier():
 
 
 def test_long_sympy_mapping():
+    sp = pytest.importorskip("sympy")
     from pymbolic.interop.sympy import SympyToPymbolicMapper
-    import sympy as sp
     SympyToPymbolicMapper()(sp.sympify(int(10**20)))
     SympyToPymbolicMapper()(sp.sympify(int(10)))
 
diff --git a/test/test_sympy.py b/test/test_sympy.py
new file mode 100644
index 0000000000000000000000000000000000000000..6f6ff69249ab2d7ad71620b59c166eac73dfe5ce
--- /dev/null
+++ b/test/test_sympy.py
@@ -0,0 +1,119 @@
+from __future__ import division
+
+__copyright__ = "Copyright (C) 2017 Matt Wala"
+
+__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.
+"""
+
+import pytest
+import pymbolic.primitives as prim
+
+x_, y_ = (prim.Variable(s) for s in "x y".split())
+
+
+# {{{ to pymbolic test
+
+def _test_to_pymbolic(mapper, sym, use_symengine):
+    x, y = sym.symbols("x,y")
+
+    assert mapper(sym.Rational(3, 4)) == prim.Quotient(3, 4)
+    assert mapper(sym.Integer(6)) == 6
+
+    assert mapper(sym.Subs(x**2, (x,), (y,))) == \
+        prim.Substitution(x_**2, ("x",), (y_,))
+    # FIXME in symengine
+    deriv = sym.Derivative(x**2, (x,)) if use_symengine else sym.Derivative(x**2, x)
+    assert mapper(deriv) == prim.Derivative(x_**2, ("x",))
+
+    # functions
+    assert mapper(sym.Function("f")(x)) == prim.Variable("f")(x_)
+    assert mapper(sym.exp(x)) == prim.Variable("exp")(x_)
+
+    # constants
+    import math
+    # FIXME: Why isn't this exact?
+    assert abs(mapper(sym.pi) - math.pi) < 1e-14
+    assert abs(mapper(sym.E) - math.e) < 1e-14
+    assert mapper(sym.I) == 1j
+
+# }}}
+
+
+def test_symengine_to_pymbolic():
+    sym = pytest.importorskip("symengine.sympy_compat")
+    from pymbolic.interop.symengine import SymEngineToPymbolicMapper
+    mapper = SymEngineToPymbolicMapper()
+
+    _test_to_pymbolic(mapper, sym, True)
+
+
+def test_sympy_to_pymbolic():
+    sym = pytest.importorskip("sympy")
+    from pymbolic.interop.sympy import SympyToPymbolicMapper
+    mapper = SympyToPymbolicMapper()
+
+    _test_to_pymbolic(mapper, sym, False)
+
+
+# {{{ from pymbolic test
+
+def _test_from_pymbolic(mapper, sym, use_symengine):
+    x, y = sym.symbols("x,y")
+
+    assert mapper(x_ + y_) == x + y
+    assert mapper(x_ * y_) == x * y
+    assert mapper(x_ ** 2) == x ** 2
+
+    assert mapper(prim.Substitution(x_**2, ("x",), (y_,))) == \
+        sym.Subs(x**2, (x,), (y,))
+    # FIXME in symengine
+    deriv = sym.Derivative(x**2, (x,)) if use_symengine else sym.Derivative(x**2, x)
+    assert mapper(prim.Derivative(x_**2, ("x",))) == deriv
+
+    assert mapper(x_[0]) == sym.Symbol("x_0")
+
+    assert mapper(prim.Variable("f")(x_)) == sym.Function("f")(x)
+
+# }}}
+
+
+def test_pymbolic_to_symengine():
+    sym = pytest.importorskip("symengine.sympy_compat")
+    from pymbolic.interop.symengine import PymbolicToSymEngineMapper
+    mapper = PymbolicToSymEngineMapper()
+
+    _test_from_pymbolic(mapper, sym, True)
+
+
+def test_pymbolic_to_sympy():
+    sym = pytest.importorskip("sympy")
+    from pymbolic.interop.sympy import PymbolicToSympyMapper
+    mapper = PymbolicToSympyMapper()
+
+    _test_from_pymbolic(mapper, sym, False)
+
+
+if __name__ == "__main__":
+    import sys
+    if len(sys.argv) > 1:
+        exec(sys.argv[1])
+    else:
+        from py.test.cmdline import main
+        main([__file__])