madhadron

Transpiling Python on the fly

Last updated: January 11, 2022

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):
    a = 3
    b = a+5
    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):
    lines, count = inspect.getsourcelines(f)
    source = ''.join(lines)
    a = ast.parse(source)
    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):
    f = globals()['_' + node.__class__.__name__]
    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):
    arglist = ', '.join(genjs(arg) for arg in node.args.args)
    body = '\n'.join(genjs(stmt) for stmt in node.body)
    return f'''
    function {node.name}({arglist}) {{
        {body}
    }}
    '''

def _Assign(node):
    assert len(node.targets) == 1 # JavaScript doesn't support destructuring
    target = genjs(node.targets[0])
    value = genjs(node.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.