I’ve been thinking about this thread, so I went back to the OP’s post and thought about what was going on.
I believe that the general problem is “how to write something in ones code, than is not to be interpreted as python, but translated to and finally processed as python”. Is this a definition of a DSL?
Consider the OP’s problem.
y = x**2 + 3*x – 5 # (1)
This looks like python and given x = 1 will evaluate to -1. However we want it to to be computed using different rules of arithmetic, i.e. modulo 11, where numbers are constrained in the range 0 to 10 or even a different bitwise logic notation where ** is exclusive or, * is and, + is or and – is inverted-or.
A python way to do this would be to define a class inheriting from integer and then to overload the operators.
However, the OP is looking for a solution that does not overload operators and evaluates the expression as function calls, so
y = cas.sub(cas.add(cas.pow(cas.name("x"), cas.constant(2)), cas.mul(cas.constant(3), cas.name("x"))), cas.constant(5)) # (2)
would be a solution, given that cas was a module that contained functions (sub, add, …) to compute all the operations as required. We then need a way to translate (1) to (2).
Using my macro tools an example code might look like this …
# coding: magic_macro
# above line invokes a preprocessor to deal with the macros.
# This is standard python using magic_codec (see pypi)
#
import cas
#
macro! cas
# extracts macro definitions from module cas
#
result = cas!$ x**2 + 3*x – 5
This would result in effectively the following code
import cas
result = cas.sub(cas.add(cas.pow(x, 2), cas.mul(3, x)), 5)
This has a compile time overhead but no runtime delay over and above calculating the function. Variable interpolation and constant references are compile time issue. Nothing is currently cached, though the macro interpreter could be built to cache the macro parameters and result to be reused when processing different parts of the code
The module cas is the key. Firstly it contains the functions sub, add, etc. Then the definition for the macro cas
def _cas(node, vargs, macro_list):
# simplified
return _translate(vargs[V.PARAMETERS])
plus some boilerplate to add _cas to the macro processor as cas
vargs[V.PARAMETERS]
is simply a string containing all the text passed to the macro.
and finally a function _translate does the work, and that has a signature
def _translate(a:sr) → ast.Node:
Note that _translate is executed at compile time NOT run time. Calling either _translate or _cas at runtime would result in an exception being raised. A runtime version could be constructed using the eval built-in i.e. result = cas.cas(‘{x}**2 + 3*{x} – 5’)
, stealing ideas from f-strings and PEP 750.
This scheme would work for any DSL, the problem is to write the _translate function which is part of cas or equivalent module, not a python built-in or part of the standard library.
DSL’s which look like python are easy-ish to translate as the DSL expression can be converted to an ast tree which is easy-ish to walk and convert to another form as required. A non pythonic DSL would need a bespoke parser to generate the ast tree.
An alternative solution might be based on ideas from PEP 750 but this would constrain the syntax of the DSL and involve lots of {} pairs and explicit strings. One might even think of PEP 750 as a very specific DSL.
One could easily work with
answer = dsl!{ a[b] | c[d]}
What does a _translate function do? Remember that answer is the result returned by processing the DSL not a reference to the code, parameters or calculation. These are only valid at compile time and are only accessible by the compiler or in my case the macro processor which is run first.
What about this?
answer = dsl!{ f(23)[a ~ b] | (a or True)[d & d]}
Its all valid python, just the origional a, b, c, d are expressions.