pydrogen
Python library for building embedded languages within Python that have alternative operational semantics or abstract interpretations.

Purpose

This library allows programmers to free the building blocks that constitute Python syntax from the default Python semantics. It is designed to allow programmers to define quickly and to use immediately one or more alternative abstract interpretation or operational semantics for Python.

Repository and Package

The official repository is maintained on GitHub. The package is also available on PyPI.

pip install pydrogen
          
Note that this library is only compatible (i.e., tested) with Python 3. It depends on the ast and inspect modules.

Examples

A user would normally import the module in the usual way.

import pydrogen
          
To create an alternative interpretation for Python syntax, it is only necessary to extend the Pydrogen class and to provide definitions for those functions that handle the abstract syntax nodes that are of interest. The complete Python abstract syntax can be found in the documentation of the Python ast module.

Below, we define a simple type checking algorithm.

class Ty(pydrogen.Pydrogen):
    def Statements(self, ss): return ss.post()[-1] # Last statement.
    def Module(self, ss): return ss.post()
    def FunctionDef(self, ss): return ss.post()
    def Return(self, e): return e.post()

    def True_(self): return 'Bool'
    def False_(self): return 'Bool'
    def BoolOp(self, es): return 'Bool'if frozenset(es.post()) == {'Bool'} else 'Error'
    def Not(self, e): return 'Bool' if e.post() == 'Bool' else 'Error'

    def Num(self, n): return 'Int'
    def Add(self, e1, e2): return 'Int' if e1.post() == 'Int' and e2.post() == 'Int' else 'Error'
    def Sub(self, e1, e2): return 'Int' if e1.post() == 'Int' and e2.post() == 'Int' else 'Error'
    def Mult(self, e1, e2): return 'Int' if e1.post() == 'Int' and e2.post() == 'Int' else 'Error'
    def USub(self, e): return 'Int' if e.post() == 'Int' else 'Error'
          
Given the above definition, we can use @Ty as a decorator on any function. This will make it possible to query that function's type at a later point using the name of the class extension defined above.

@Ty
def correct():
    return -1 + 2 - 3 * 4

print("The type of 'correct' is " + str(correct.Ty) + ".")

@Ty
def incorrect():
    return 123 and False

print("The type of 'incorrect' is " + str(incorrect.Ty) + ".")
          
The example below illustrates how multiple interpretations can be used together by defining a running time approximation algorithm for a very small subset of Python.

class Size(pydrogen.Typical):
    def List(self, elts): return len(elts.post())
    def Num(self, n): return 1

class Time(pydrogen.Typical):
    def Statements(self, ss): return sum(ss.post())
    def Assign(self, targets, e): return 1 + e.post()
    def For(self, target, itr, ss, orelse): return Size(itr.pre()) * (1 + ss.post())
    def BoolOp(self, es): return 1 + sum(es.post())
    def BinOp(self, e1, e2): return 1 + e1.post() + e2.post()
    def Call(self, func, args): return 1 + sum(args.post())
    def Num(self, n): return 0
    def NameConstant(self): return 0

@Time
def example():
    for x in [1,2,3,4,5,6,7]:
        print(True and False)
        print(123 + 456)

print("The approximate running time of example is " + str(example.Time) + ".")
          
The example below illustrates how the optional context parameter can be used to create environments. Note that because only the statement handlers return (env, ty) tuples, we only need to handle such tuple results when they appear (while not worrying about them when dealing with results from expression handlers, which do not return an environment).

class Ty2(pydrogen.Typical):
    def Statements(self, ss, env):
        (tys, env) = ss.post(env)
        return tys[-1]
    def Assign(self, targets, value, env):
        var = targets.pre()[0].id # Only single variable assignment.
        envNew = env.copy()
        envNew[var] = value.post(env)
        return ('Void', envNew)
    def Name(self, x, env):
        return env[x.pre()] if x.pre() in env else 'Error'
    def Num(self, n, env):
        return 'Int'
    def Add(self, e1, e2, env):
        return 'Int' if {e1.post(env), e2.post(env)} == {'Int'} else 'Error'

@Ty2
def example2():
    y = 0
    x = 1 + 2 + 3
    return x + y

print("The type of example2() is " + str(example2.Ty2) + ".")