lcompilers / lpython

Python compiler
https://lpython.org/
Other
1.51k stars 164 forks source link

Symbolics TODO #2332

Open certik opened 1 year ago

certik commented 1 year ago

The goal is to design a very small and clean ASR design. As few functions and ASR nodes as possible, the S type does not have methods, we have global ASR nodes (as few as possible) and IntrinsicFunctions (can be many) to operate on it. We want to keep this Symbolic API surface to be minimal and clean (no duplication).

Let's write a pure Python implementation of this Symbolic API, initially we'll just ship it with LPython. It will implement exactly this API in pure CPython, so we will ensure 100% compatibility (thus satisfying the LPython contract that if it works in LPython, it gives exactly the same result in CPython). SymPy itself will be about 95% compatible, so most tests will work in both and we will continue testing both, but in some corner cases (such as exp(x).func, and sin(pi/8) etc.) it will differ, so those tests will only test in our pure Python implementation (we will ensure that majority of tests work in SymPy). And then much later, we'll ensure that the symengine.py (Python wrappers to SymEngine) are a strict superset of our "lean Symbolic API", so that one can use symengine.py interchangeably with our pure Python implementation (and it will be faster, while still being in CPython). And we will carefully document all differences to SymPy. The contract will eventually be that if a symbolic code compiles and works with LPython, it will automatically work with our pure Python implementation as well as with symengine.py 100% of the time, and with SymPy only about 95%. And we'll offer our pure Python implementation (which will be slower than symengine.py, but faster than current SymPy) to SymPy itself, and using LPython SymPy can gradually move to this new core, we can start with limits.

anutosh491 commented 1 year ago

Well if we plan on using basic_get_args for implemnting e.exp and e.base (instead of what I was trying to do in https://github.com/symengine/symengine/pull/1980), we would need a work around for the CVecBasic type just like we had one for the basic type. If you remember, we introduced a workaround for declaring a basic variable as follows

    _x: i64 = i64(0)
    x: CPtr = empty_c_void_p()
    p_c_pointer(pointer(_x, i64), x)

Now what basic_get_args does is, it takes in a pointer to CVecBasic and then it populates the value of args with the arguments.

//! Returns a CVecBasic of vec_basic given by get_args
CWRAPPER_OUTPUT_TYPE basic_get_args(const basic self, CVecBasic *args);

Hence we might need to think about a workaround for implementing CVecBasic in LPython. Here it is a bit different that the general procedure for a basic variable(which is to define it through CPtr and then use the basic_new_stack function )

Here we have something like

    CVecBasic *args = vecbasic_new();
    basic_get_args(e, args);
certik commented 1 year ago

You can also write an auxiliary C function that does this, if it simplifies things.

certik commented 1 year ago

Regarding the args, let's first implement the following:

args: list[S] = (x**2).args
print(args[0], args[1])

Where we obtain the whole vector of arguments from SymEngine, and we convert it to a Python tuple. Then we just deal with a tuple. For that, we should first get tuples of type S working in LPython, that is, get this working first:

args: list[S] = (x, S(2))
print(args[0], args[1])

And then once that works, let's get args: list[S] = (x**2).args working. For that, I think we need some ASR function, let's say SymbolicGetArguments that accepts an expression like x**2 and returns a tuple[S] with the arguments.

I think after that, the following will also automatically start working:

print((x**2).args[0], (x**2).args[1])

However, for this to work, tuple would have to have a runtime size. Currently in LPython we have a compile time size, and in fact tuple[S] is a tuple of just one element. tuple[S, S, S] is a tuple of 3 elements. We have two ways forward:

Once symengine gives us the CVecBasic *args, then you can construct the following ASR code to construct a list[S], as an implementation of SymbolicGetArguments, in Python:

r: list[S] = []
args = (x**2).args
for i in range(len(args)):
    r.append(args[i])

This has to be done in LPython using symengine's C API for handling the CVecBasic structure. So there is a function get gets you the length (len(args)) and there is another function to access an i-th element of the vector (args[i]).

certik commented 1 year ago

I would add a new integration test test_gruntz.py and in there I would start implementing the mrv function, I would start small and then start adding things to it. Every time we discover something that does not work, I would isolate the bug, and add a dedicated separate test test_symbolics_??.py for it. That way every single new feature will be tested. And then in test_gruntz.py I would just keep improving the one mrv implementation iteratively, whatever is there will be always working, and step by step we will get it to the point that it is equivalent (or almost equivalent) to SymPy's mrv: https://github.com/sympy/sympy/blob/b6e81e7c3f3860c7e826018e587531a8622fcc4e/sympy/series/gruntz.py#L248.