SHARE
TWEET

Meta-Programming in RPython

a guest Aug 3rd, 2010 123 Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. ## Meta-Programming in RPython by Goathead - goatman.py@gmail.com
  2. import os, sys, time, inspect, types, select, subprocess, math, pickle, random
  3.  
  4. '''
  5. from: http://pyppet.blogspot.com/2010/08/meta-programming-in-rpython.html
  6.  
  7. RPython Language Restrictions:
  8.         1. Lists and Dicts must contain a compatible type.
  9.                 (objects with a common base class are compatible)
  10.         2. Function arguments types must not be changed after the first call, each call must be consistent with the others in the ordering of types.
  11.                 (this also applies to function returns)
  12.         3. Subclasses must redefine functions that interact with an attribute they have made a new type.*
  13.         4. String and char are different, any string of length 1 becomes a char.  (some builtin functions accept only a char)
  14.         5. Tuples may have incompatible types, but can not be iterated over, yet. (the error messages suggest the iteration limitation is going away)
  15.         6. Only __init__ and __del__ are allowed, so no custom attribute access or operator overloading.
  16.         7. Globals are considered constants (use singletons for global changeable state)
  17.         8. No runtime definition of functions or classes.
  18.         9. Slicing can not use negative indexes except for [:-1]
  19.         10. Some builtin functions requires literals, like getattr, for example:
  20.                 valid:
  21.                         getattr(o,'x')
  22.                 invalid:
  23.                         arg = 'x'
  24.                         getattr( o, arg )
  25.  
  26. There are a few other rules and exceptions not listed above, but those are small and the RPython translator/compiler is going to give you clear errors when you violate them.  Getting started with RPython it is easy to make a mistake somewhere and violate rules 1-3, the errors you will get from breaking them are a bit cryptic as well.
  27.         a. Exception: don't know how to simply render py object: <type 'some-type'>
  28.         b. pypy.rpython.error.TyperError: don't know how to convert from <InstanceRepr for [mytype]> to <PyObjRepr * PyObject>
  29.         c. AttributeError: no field 'll_newlist'
  30.         d. pypy.rpython.error.TyperError: reading attribute '__init__': no common base class for
  31.  
  32. *Rule 3 is unusal and easy to overlook, it means we must be careful with functions defined in our base class that interact with attributes we have redefined in subclasses that are of a new type.  In other words, a function defined in the base class expects its the attributes of self to always have those fixed types,  but if the subclasses changes the type, the function breaks.  In unrestricted Python we would normally want to put as many shared functions in the base class as possible to avoid code duplication, rule 3 turns this upside down.  Consider the following where we use wrapper objects for generic types and note that we have put the unwrap function in each of the subclasses.
  33.  
  34. '''
  35. class Type1(object): pass
  36. class Type2(object): pass
  37.  
  38. class Abstract(object):
  39.         def __init__(self):             # be careful with init, do not try to define defaults for what will have different types in subclasses!
  40.                 #self.x = 'never define what x is in the base class'
  41.                 self.y = 'this is safe'
  42.         def failure(self): self.x = 1.0
  43.  
  44. class O(object):
  45.         def __init__(self, arg='string'): self.a = arg
  46.  
  47. class X( Abstract ):
  48.         def __init__(self):
  49.                 Abstract.__init__(self)
  50.                 self.x = Type1()
  51.         def unwrap(self): return self.x
  52.  
  53. class Y( Abstract ):
  54.         def __init__(self):
  55.                 Abstract.__init__(self)
  56.                 self.x = Type2()
  57.         def unwrap(self): return self.x
  58.  
  59. class Z( Abstract ):
  60.         def __init__(self):
  61.                 Abstract.__init__(self)
  62.                 self.x = 10.0
  63.         def unwrap(self): return self.x
  64.  
  65. class W( Abstract ):
  66.         def __init__(self):
  67.                 Abstract.__init__(self)
  68.                 self.x = O
  69.         def unwrap(self): return self.x
  70.         def new_instance(self, arg): return self.x(arg)
  71.  
  72. def test_subclasses_fails():
  73.         c = Abstract()
  74.         c.failure()     # this breaks because the type can not be known from the base class
  75.         a = X()
  76.         b = Y()
  77.  
  78. def test_subclasses_works():
  79.         ab=Abstract()
  80.         a = X()
  81.         b = Y()
  82.         c = Z()
  83.         lst = [a,b]     # sharing the same base class means they can co-exist in a list
  84.         for item in lst: print item
  85.         print a.x
  86.         print b.x
  87.         print c.x
  88.         o = W().x()
  89.         print o
  90.         print a.unwrap()
  91.         print b.unwrap()
  92.         o2 = W().new_instance( 'hello world' )
  93.         print o2.a
  94.         print 'test shared complete'
  95.  
  96. '''
  97. Another case that can easily lead to rule breaking is when dealing with *args which is a tuple,  and since tuples can store incompatibles, we may be tempted to use it to pass variable amounts of incompatible types; but they can not be iterated over, and casting args to a list will not work either because lists must contain only compatible types.  Tuple indexing must also be done with a literal constant as well, so for loop will fail:
  98. def func( *args ):
  99.         for i in range(len(args)):
  100.                 a = args[i]
  101.  
  102. Above fails, so the only way to get at items in args is to index each one with a literal like this, not pretty!:
  103. n = len(args)
  104. if n > 1: args[0]
  105. if n > 2: args[1]
  106. if n > 3: args[2]
  107.  
  108. If we have to code that way, then it seems like we should give up trying to pass variable numbers of incompatible types.  However, unlike many other languages which are parsed only from source code, RPython uses live objects and introspection; and before compiling we have the chance to generate classes, modify functions, create new globals, and more, all from unrestricted Python.  Using meta-programming we can easily bend some of the restrictions of RPython, in this case we are going to unroll the loop and inline valid RPython code.
  109.  
  110. '''
  111. STAR_TYPES = 'bool int float str tuple list Meta ID'.split()
  112. ARGUMENT_SEP = '~'
  113. class ID(object):
  114.         def __init__(self,value='undefined'): self.value = value
  115.  
  116. class Meta(object): pass
  117.  
  118. class SimplePack( Meta ):               # would be useful to turn this into a minipickler
  119.         def init_head(self):
  120.                 self.ID = '0'
  121.                 self._init_string = ''
  122.                
  123.         def init_bool(self, v ): self._init_string += '%s%s' %(v, ARGUMENT_SEP)
  124.         def init_int(self, v ): self._init_string += '%s%s' %(v, ARGUMENT_SEP)
  125.         def init_float(self, v ): self._init_string += '%s%s' %(v, ARGUMENT_SEP)
  126.         def init_str(self, v ): self._init_string += '"%s"%s' %(v, ARGUMENT_SEP)
  127.         def init_tuple(self, v ): self._init_string += '%s%s' %(v, ARGUMENT_SEP)
  128.         def init_list(self, v ): self._init_string += '%s%s' %(v, ARGUMENT_SEP)
  129.         def init_Meta(self, v ): self._init_string += '@%s%s' %(v.ID, ARGUMENT_SEP); print 'got meta'
  130.         def init_ID(self, v ): self.ID = v.value; print 'got id'
  131.  
  132. '''
  133. The SimplePack subclass above is going to handle each possible type that may be passed in *args from our generated function.  gen_star_func and gen_switch_block are going to create the unrolled function for us, that can be very large depending on what maxitems is set to as we will see below.
  134. '''
  135.  
  136. def gen_switch_block( prefix, indent, index ):
  137.         indent += '  '
  138.         r = ''
  139.         for type in STAR_TYPES:
  140.                 if not r:
  141.                         r += '%sif isinstance( args[%s], %s ): self.%s_%s(args[%s])\n' %(indent,index,type, prefix,type,index)
  142.                 else:
  143.                         r += '%selif isinstance( args[%s], %s ): self.%s_%s(args[%s])\n' %(indent,index,type, prefix,type,index)
  144.         r += '%selse: print "unknown type", args[%s]\n' %(indent,index)
  145.         return r
  146.  
  147. def gen_star_func(  name, handler_prefix='init', head=None, tail=None, maxitems=16 ):
  148.         ident = '  '
  149.         if head: body = '  self.%s\n' %head
  150.         else: body = ''
  151.         body += '  n = len(args)\n'
  152.         for i in range( maxitems ):
  153.                 body += '%sif n > %s:\n%s' %(ident, i, gen_switch_block(handler_prefix, ident,i) )
  154.                 ident += '  '
  155.         if tail: body += '\n  ' + tail
  156.         e = 'def star_func(self, *args):\n' + body
  157.         print e
  158.         exec( e )
  159.         star_func.func_name = name
  160.         return star_func
  161.  
  162. '''
  163. The output of gen_star_func simply checks for each possible type, and forwards it to a handler.  In terms of speed there should not be a big expense, since the next switch block is only executed if the length of *args is greater, we lose some memory but it is reasonable as long as maxitems is not too high.  As you can see below the output of gen_star_func greatly helps keeps our code maintainable not having to copy and paste into every function where we want to use *args and variable types.
  164.  
  165. def star_func(self, *args):
  166.  self.init_head()
  167.  n = len(args)
  168.  if n > 0:
  169.    if isinstance( args[0], bool ): self.init_bool(args[0])
  170.    elif isinstance( args[0], int ): self.init_int(args[0])
  171.    elif isinstance( args[0], float ): self.init_float(args[0])
  172.    elif isinstance( args[0], str ): self.init_str(args[0])
  173.    elif isinstance( args[0], tuple ): self.init_tuple(args[0])
  174.    elif isinstance( args[0], list ): self.init_list(args[0])
  175.    elif isinstance( args[0], Meta ): self.init_Meta(args[0])
  176.    elif isinstance( args[0], ID ): self.init_ID(args[0])
  177.    else: print "unknown type", args[0]
  178.    if n > 1:
  179.      if isinstance( args[1], bool ): self.init_bool(args[1])
  180.      elif isinstance( args[1], int ): self.init_int(args[1])
  181.      elif isinstance( args[1], float ): self.init_float(args[1])
  182.      elif isinstance( args[1], str ): self.init_str(args[1])
  183.      elif isinstance( args[1], tuple ): self.init_tuple(args[1])
  184.      elif isinstance( args[1], list ): self.init_list(args[1])
  185.      elif isinstance( args[1], Meta ): self.init_Meta(args[1])
  186.      elif isinstance( args[1], ID ): self.init_ID(args[1])
  187.      else: print "unknown type", args[1]
  188.      if n > 2:
  189.        if isinstance( args[2], bool ): self.init_bool(args[2])
  190.        elif isinstance( args[2], int ): self.init_int(args[2])
  191.        elif isinstance( args[2], float ): self.init_float(args[2])
  192.        elif isinstance( args[2], str ): self.init_str(args[2])
  193.        elif isinstance( args[2], tuple ): self.init_tuple(args[2])
  194.        elif isinstance( args[2], list ): self.init_list(args[2])
  195.        elif isinstance( args[2], Meta ): self.init_Meta(args[2])
  196.        elif isinstance( args[2], ID ): self.init_ID(args[2])
  197.        else: print "unknown type", args[2]
  198.        if n > 3:
  199.          ...(to maxitems)
  200.  
  201.  
  202. '''
  203.  
  204.  
  205. def gen_class( name, base=Meta, items={} ):
  206.         cls = types.ClassType(str(name), bases=(base,), dict=items)
  207.         globals().update( {name:cls} )
  208.         return cls
  209.  
  210. def gen_metas( names ):
  211.         generated = []
  212.         init = gen_star_func( '__init__', handler_prefix='init', head='init_head()' )
  213.         for name in names.split():
  214.                 items = {'__init__':init}
  215.                 cls = gen_class( name, base=SimplePack, items=items )
  216.                 generated.append( cls )
  217.         return generated
  218. GEN_METAS = gen_metas( 'Meta1 Meta2 Meta3' )
  219.  
  220. def test_meta():
  221.         m1 = Meta1()
  222.         myid = ID('myid')
  223.         print myid
  224.         m2 = Meta2('a', 'b', 1.0, 'string', m1, myid )
  225.         print m2._init_string
  226.         m3 = Meta3( m1, m2 )
  227.         print m3._init_string
  228.  
  229. '''
  230. The test above test_meta takes the variable argument types and packs them into a string that could be sent over a pipe or socket for communication with another process.
  231.  
  232. To run these tests you need to download the pypy source code, and set PATH2PYPY to where pypy is.
  233. Below the tests function is the pypy entry point, to run the different tests above uncomment test_*
  234.  
  235. '''
  236. PATH2PYPY = 'pypy'
  237. sys.path.append(PATH2PYPY)              # assumes you have pypy dist in a subfolder, you may need to rename this pypy-trunk
  238.  
  239. def tests( outside ):
  240.         #test_subclasses_fails()       
  241.         #test_subclasses_works()
  242.         test_meta()
  243.  
  244. from pypy.translator.interactive import Translation
  245. t = Translation( tests )
  246. t.annotate([str]); t.rtype()
  247. f = t.compile_c()
  248. f('from the outside')
  249.  
  250. print( 'end of program' )
RAW Paste Data
Top