Transpiling Python on the fly
Performing black magic with your language runtime is fun. For example, here’s a little toy that will make perfect sense if you ever dealt with a parser or a compiler, but that many people might not of thought of. We can take a function defined in Python and transpile it to JavaScript at runtime.
This works because we have the modules inspect
(to get
information about objects in the Python runtime) and ast
(to parse Python code into an abstract syntax tree) in the standard
library. We won’t do the whole language, just enough to demonstrate the
point.
Let’s start with a function to transpile
def f(a,b):
= 3
a = a+5
b return a+b
and we’ll set ourselves up to get an abstract syntax tree from it
import inspect
import ast
def ast_of(f):
= inspect.getsourcelines(f)
lines, count = ''.join(lines)
source = ast.parse(source)
a return a
If we explore a
we find (in a syntax kind of like JSON
with class names prepended to objects)
Module {
body: [
FunctionDef {
name: 'f',
args: [Arg {arg: 'a'}, Arg {arg: 'b'}]
body: [
Assign {
targets: [Name {id: 'a'}],
value: Constant {value: 3}
},
Assign {
targets: [Name {id: 'b'}],
value: BinOp {
left: Name {id: 'a'},
op: Add {},
right: Constant {value: 5}
}
},
Return {
value: BinOp {
left: Name {id: 'a'},
op: Add {},
right: Name {id: 'b'}
}
}
]
}
]
}
Now let’s transform it. We’re going to define functions that map a
given kind of syntax element to JavaScript. Then when we hit that syntax
element, we call the corresponding function. We could define a dict to
map classes to functions, but that’s too much work when we have a
perfectly good dict already defined: globals()
.
def genjs(node):
= globals()['_' + node.__class__.__name__]
f return f(node)
If we pass a
to genjs
, it gets the name of
the class from a
(which is 'Module'
) and looks
for a function named _Module
in the current module, then
calls it on the node. So we just need to implement the functions for
each syntax element that appears in our function. For clarity of the
code I will make no attempt to nicely indent the result.
def _Module(node):
return '\n\n'.join(genjs(stmt) for stmt in node.body)
def _FunctionDef(node):
= ', '.join(genjs(arg) for arg in node.args.args)
arglist = '\n'.join(genjs(stmt) for stmt in node.body)
body return f'''
function {node.name}({arglist}) {{
{body}
}}
'''
def _Assign(node):
assert len(node.targets) == 1 # JavaScript doesn't support destructuring
= genjs(node.targets[0])
target = genjs(node.value)
value return f'let {target} = {value};'
def _Num(node):
return str(node.n)
def _BinOp(node):
return f'{genjs(node.left)} {genjs(node.op)} {genjs(node.right)}'
def _Add(node):
return '+'
def _Return(node):
return f'return {genjs(node.value)};'
def _Name(node):
return node.id
Finally we run print(genjs(ast_of(f)))
and get
function f(a, b) {
let a = 3;
let b = a + 5;
return a + b;
}
There’s nothing special about JavaScript here. We could produce any
other output our heart desired, or count up the number of
if
statements in a call tree. Filling out enough to
transpile a useful subset of Python, maybe Skylark, would be a fair
amount of work, but there is nothing new in it beyond what is here.