Background: This is the result of a discussion between DagSverreSeljebotn and MartinCMartin; see here. Long-term it would be an approach to unify CEP 511 (macro meta-programming) with CEP 508 (compile-time unrolling meta-programming). (The latter is a bit poorly explained in the wiki, however if you are really interested read the emails linked to for a better explanation.)
The word macro everywhere refers to the LISP meaning (not the much less ambitious C meaning, which is basically covered by inline functions).
In the below I'll assume
1 from cython.meta import uneval, codenode, PLUS, EXP, VALUE, VAR, CALL, MULTIPLY
What is the uneval psuedo-function?
It is a compile-time code introspection tool. Basically, it takes an expression and returns a tree representing the code that computes the expression. The tree can then be modified, and eval() called on it.
This kind of behaviour cannot happen at any time though. For now the suggestion is to only allow use within macros as described in the next section, however limited support could be added for use within functions as well. For now, I'll use the cython.macro keyword to specify a macro.
For demonstration purposes only, consider however the following code-snippet (while completely ignoring feasability, which is the subject of the next section!):
1 >>> a = 23 2 >>> b = a ** global_var - sin(23) 3 >>> tree = uneval(b) 4 >>> print repr(tree) 5 codenode(name="b", op=PLUS, args=[ 6 codenode(name=None, op=EXP, args=[ 7 codenode(name="a", op=VALUE, args=), 8 codenode(name="global_var", op=VAR, args=) 9 ]), 10 codenode(name=None, op=CALL, args=[sin, 11 codenode(name=None, op=VALUE, args=) 12 ]) 13 ])
Notice that a and b has their name attribute set, while "temporaries" in the calculation has no name.
What should happen if the value is determined by if-tests, for-loops etc. is a tough question that's not dealt with now. However, note that at any time the tree can be "cut short" and provide a "magic black box" for some values (ie like global_var above). So either the code tree is extended to follow the program flow back through if-tests and for-loops, or it is cut off and only contain the resulting calculations as op=VAR.
The code tree should overload str in order to provide nice formatting.
1 >>> #...continuing... 2 >>> print str(tree) 3 a = 23 4 b = a ** global_var - sin(23) 5 b
(Or "return b"...not sure...) Note that this is a "reformatting" of the tree so might not be identical with the input code snippet.
After the desired modifications etc. are made to the tree, it can be injected back into the Python code by using the eval() function.
1 >>> #...continuing... 2 >>> print tree.eval() 3 # same result as "print b"
On eval(), side-effects will be fired again up to the first named node in the tree. Ie:
1 >>> def a(x): 2 ... print "Hello", x 3 ... return 1 4 ... 5 >>> 6 >>> b = a(1) 7 Hello 1 8 >>> tree = uneval(a(2) + a(3) + b + b) 9 >>> # Notice that the parameter to uneval is not calculated yet, no side-effects 10 >>> print tree.eval() 11 Hello 2 12 Hello 3 13 4 14 >>> print tree.eval() 15 Hello 2 16 Hello 3 17 4
Note: In macros below, it is actually wanted to have side-effects run one level further down, ie basically ignore that the root node is named. This must be solved somehow (a parameter "param=True" in the root would do, but perhaps one can do better).
Real use: Macros
Of course, this functionality cannot be used in any run-time code. All calls to code-trees must "disappear" at compile-time. That's where CEP 508 enters. In CEP 508 the decorator @cython.unroll is used, but below I'll use @cython.macro.
With the addition of uneval, CEP 508 can provide much macro behaviour. The main difference to CEP 511 is that the calling convention is similar to Python (the expressions themselves go in and out) so one can keep the def keyword.
1 @cython.macro 2 def deriv(x, var): 3 node = uneval(x) 4 if node.op == ADD: 5 node.args = [deriv(x) for x in node.args] 6 elif node.op = MULTIPLY: 7 node.op = ADD 8 node.args = [ 9 codenode(MULTIPLY, [node.args, deriv(node.args)]), 10 codenode(MULTIPLY, [deriv(node.args), node.args]) 11 ] 12 elif node.name == var: 13 return codenode(VALUE, ) 14 else: 15 return codenode(VALUE, ) 16 17 # node now represents the derivated expression 18 # return it _by value_ to the caller 19 return node.eval()
Note that the above would remove any side-effects coming from expressions that are derivated away, ie:
1 deriv(3*x*x + sin(34), "x")
Here, sin is never even called. In order to always have side-effects run, one can insert an extra node.eval() before modifying the tree (in the case above, one would insert "node.eval()" lines before returning 1 and 0, in order to fire off side-effects but discard the results, since that is when nodes are dropped).
This gets much more complicated as one starts adding expressions that can only be known run-time into the macros. For the full version, please read the mentioned emails first for much more discusssion about this.
At least it is clear that if, for instance, instead of "x" above a string that is only known run-time is passed, a compile-time error must be raised.