
I'm finally starting to get my head wrapped around the compiler package, and thought I'd pass some notes on to the group since it's a beast: 1) Other than the original AST generation, it looks like it's all pure python (not tying into the C source). The original AST comes from the parser module, which is a pure wrapper module (although I've already posted python code to build ast's here) 2) Internal CPython ASTs are transformed into "compiler"'s own ASTs. These nodes are smarter than the internal ones; they attach appropriate info to "attributes". CPython AST nodes just have a list of children, this module identifies and breaks out important data (for example, the arguments in a function def become an "args" attribute instead of guessing it's the first child) 3) The CodeGenerators walk the ASTs and emit code into a code object. 4) Speed seems reasonable (which makes me think I'm missing some calls into the C side of things) 4) The generated bytecode looks reasonably good and execs fine, but there are still some diffences from what CPython generates. Here's what I've seen so far: SET_LINENO doesn't always work the same (this is how python knows what line in a file threw an exception) Doesn't have one of the few optimizations the CPython compiler does have. CPython will throw out misplaced "docstrings"... strings in the source that aren't assigned to anything. This throws off array indexes in the code objects co_const attribute. In general, the compiler package is in alot better shape than I expected. If anyone is interested in poking around, here's a quick script that does basic comparison and diff on the bytecode generated by the builtin compile and compiler's equivilent function. I'll probably expand this to compare the whole code objects in the near future. =============== CompilerTest.py =============== import sys import compiler import dis def opcodeTuples(source): """ Makes Opcode tuples in the form of: OFFSET, NAME, [OPTIONAL PARAM] """ retVal = [] a = iter(source) offset = 0 def getByte(next=a.next): return ord(a.next()) def getWord(next=a.next): return ord(a.next()) + ord(a.next()) * 256 # Little-endian while 1: try: opcode = getByte() opname = dis.opname[opcode] if opcode < 90: retVal.append( (offset, dis.opname[opcode]) ) offset += 1 else: retVal.append( (offset, dis.opname[opcode], getWord())) offset += 3 except StopIteration: break return retVal def opcodeDiff(ops1, ops2): """ Does a simple DIFF of two sets of opcodes. Can only check one skipped line. Ignores param for now since they don't match """ opcode1, opcode2 = opcodeTuples(ops1), opcodeTuples(ops2) a,b = 0,0 print "%30s%30s" % ("FIRST", "SECOND") print "%30s%30s" % ("====================" ,"====================") while 1: if opcode1[a][1:2] == opcode2[b][1:2]: print "%30s%30s" % (opcode1[a], opcode2[b] ), if opcode1[a][2:] != opcode2[b][2:]: print " ARG MISMATCH" else: print a += 1 b += 1 elif opcode1[a+1][1:2] == opcode2[b][1:2]: print "%30s%30s" % (opcode1[a], "<not here>") a += 1 elif opcode1[a][1:2] == opcode2[b+1][1:2]: print "%30s%30s" % ("<not here>", opcode2[b]) b += 1 else: print "NONTRIVIAL DIFF%25s%25s" % (opcode1[a], opcode2[b]) break if a >= len(opcode1) and b >= len(opcode2): break elif a >= len(opcode1) or b >= len(opcode2): print "UNEXPECTED END OF OPCODES" break def compareCompiles(filename, nativeCompile=compile, pythonCompile=compiler.compile): """ Compares a bytecode compile between the native python compiler and the one written in python """ source = file(filename).read() native = nativeCompile(source, filename, "exec") python = pythonCompile(source, filename, "exec") if native.co_code == python.co_code: print "compiles matched" else: print "compiles didn't match" opcodeDiff(native.co_code, python.co_code) if __name__ == "__main__": compareCompiles("c:\\python23\\lib\\code.py") =============== End CompilerTest.py =============== --------------------------------------- Get your free e-mail address @zworg.com

logistix wrote:
I'm finally starting to get my head wrapped around the compiler package, and thought I'd pass some notes on to the group since it's a beast:
1) Other than the original AST generation, it looks like it's all pure python (not tying into the C source). The original AST comes from the parser module, which is a pure wrapper module (although I've already posted python code to build ast's here)
Oh, is that true? Last time, when I analysed what it would take to build a compiler for Python in Python, I ended up in the parser module being called, finally, which called back into the internals stuff. This was for Python 2.2.2; maybe this changed now? ciao - chris

logistix <logistix@zworg.com> writes:
I'm finally starting to get my head wrapped around the compiler package, and thought I'd pass some notes on to the group since it's a beast:
As the person who did some fixing up so it worked in 2.3a1, I second that.
1) Other than the original AST generation, it looks like it's all pure python (not tying into the C source). The original AST comes from the parser module, which is a pure wrapper module (although I've already posted python code to build ast's here)
Yep.
2) Internal CPython ASTs are transformed into "compiler"'s own ASTs. These nodes are smarter than the internal ones; they attach appropriate info to "attributes". CPython AST nodes just have a list of children, this module identifies and breaks out important data (for example, the arguments in a function def become an "args" attribute instead of guessing it's the first child)
Yep. This is compiler.transformer. It has to be said, if you were writing a parser for Python in Python, I'd shoot for generating the output of the transformer module rather than aping what the parser module produces.
3) The CodeGenerators walk the ASTs and emit code into a code object.
Yup.
4) Speed seems reasonable (which makes me think I'm missing some calls into the C side of things)
You obviously haven't tried to compile anything big with it yet. It's slow.
4) The generated bytecode looks reasonably good and execs fine, but there are still some diffences from what CPython generates. Here's what I've seen so far: SET_LINENO doesn't always work the same (this is how python knows what line in a file threw an exception)
Hey, in 2.3 SET_LINENO doesn't even exist any more :-)
Doesn't have one of the few optimizations the CPython compiler does have. CPython will throw out misplaced "docstrings"... strings in the source that aren't assigned to anything. This throws off array indexes in the code objects co_const attribute.
Yeah, indices into co_consts are different (and not just in the case you mention above, I think). But it works -- you can compile the stdlib with in and run the test suite happily.
In general, the compiler package is in alot better shape than I expected.
What were you expecting? Cheers, M.

From: "Michael Hudson" <mwh@python.net>
2) Internal CPython ASTs are transformed into "compiler"'s own ASTs. These nodes are smarter than the internal ones; they attach appropriate info to "attributes". CPython AST nodes just have a list of children, this module identifies and breaks out important data (for example, the arguments in a function def become an "args" attribute instead of guessing it's the first child)
Yep. This is compiler.transformer. It has to be said, if you were writing a parser for Python in Python, I'd shoot for generating the output of the transformer module rather than aping what the parser module produces.
yes, btw there have been undergoing work to substitute parser module and what is used internally by CPython with a (new) AST format: [Compiler-sig] compiler-sig project for Python 2.3: new AST http://mail.python.org/pipermail/compiler-sig/2002-March/000091.html http://cvs.sourceforge.net/cgi-bin/viewcvs.cgi/python/python/nondist/sandbox... t/ there should be also a CVS branch Finn Bock has already incorparated this in Jython, OTOH I think work on CPython has stalled. The idea is that future versions of (e.g) PyChecker ought to use the (new) AST format. Jeremy Hylton should be asked on the status of this. regards.

"Samuele Pedroni" <pedronis@bluewin.ch> writes:
From: "Michael Hudson" <mwh@python.net>
Yep. This is compiler.transformer. It has to be said, if you were writing a parser for Python in Python, I'd shoot for generating the output of the transformer module rather than aping what the parser module produces.
yes, btw there have been undergoing work to substitute parser module and what is used internally by CPython with a (new) AST format:
[Compiler-sig] compiler-sig project for Python 2.3: new AST http://mail.python.org/pipermail/compiler-sig/2002-March/000091.html
http://cvs.sourceforge.net/cgi-bin/viewcvs.cgi/python/python/nondist/sandbox... t/
there should be also a CVS branch
There is: ast-branch. I'd forgotten about that. Finishing that stuff up might make this project easier...
Finn Bock has already incorparated this in Jython, OTOH I think work on CPython has stalled.
Yes.
The idea is that future versions of (e.g) PyChecker ought to use the (new) AST format.
Jeremy Hylton should be asked on the status of this.
I think he's stupidly busy. Python developers really shouldn't be allowed to have kids <wink>. Cheers, M. -- If trees could scream, would we be so cavalier about cutting them down? We might, if they screamed all the time, for no good reason. -- Jack Handey

There is: ast-branch. I'd forgotten about that. Finishing that stuff up might make this project easier...
If someone could help Jeremy with that, that would be great! It's close to completion, but he's had no time to work on it.
Jeremy Hylton should be asked on the status of this.
I think he's stupidly busy. Python developers really shouldn't be allowed to have kids <wink>.
Actually, the kids have little to do with it. It's the Zope work that's killing us here at PL. --Guido van Rossum (home page: http://www.python.org/~guido/)

I've been intending to post some comments in this thread for quite a while, but both Zope and kids have been keeping my busy <wink>. The compiler package is pretty functional. In Tools/compiler there is a regrtest.py script that compiles the entire standard library with the compiler package and runs the test suite. There are a a failure related to a bug in the builtin compiler (improper handling of the unary negative optimization), but otherwise the tests all run. The package is more complex than I would like. In particular, the assembler phase that converts from abstract bytecode to concrete bytecode is rather baroque. But it is functional. I believe the stack depth computation is still pretty bogus, although Mark Hammond did a good job of getting it mostly correct. The difference between the two compilers is that the builtin compiler tracks stack depth at the same time it emits bytecode and the compiler package tries to determine the stack depth by scanning the bytecode in a later pass. The latter approach should probably compute stack depth for each basic block and then do simple flow analysis (I think already present) to determine what the max stack depth on any pass is. Even if the post-processing approach gets fixed, I'm not sure which approach I like better. As Samuele mentioned, there's an improved AST on the ast-branch and it's already being used by Jython. I don't recall the specific differences off the top of my head, but the new AST has slightly simpler data structures and is a better more regular. The ast-branch still requires a lot of work to finish, although it is functional enough to compile simple functions (definition and call). The symbol table pass is much cleaner. In general, there's less code because the AST is easier to work with. As a simplification, I decided not to do anything to change the parser and instead focused only on the backend. I had grand plans of replacing the parser in a future release, but we'll have to wait and see :-). To summarize, briefly, what remains to be done on that branch (although I'm not sure it's relevant to pypy-dev): - Conversion of concrete to abstract trees (90% done) - Error handling during conversion (basically not done) - Marshal API to pass AST between Python and C (30% done) - Basic bytecode generation (80% done) - Error checking (25% done) It's probably a couple of weeks effort to get it to alpha quality. The question I'd really like to ask, which I'll just throw out for now, is: Why would minimal python want to generate bytecode for the old interpreter? It seems the a simpler bytecode format, e.g one that didn't have a BINARY_ADD but generated approriate code to call __add__, would be a better target. There's a lot of complex C code hiding inside BINARY_ADD, which ought to get pushed back out to the Python code. A simpler and slimmer bytecode format seems like it would expose more opportunity for optimization. Jeremy

The question I'd really like to ask, which I'll just throw out for now, is: Why would minimal python want to generate bytecode for the old interpreter?
Is byte-code compatibility a goal of minimalPython? I haven't seen it mentioned anywhere but everyone seems to be working under that assumption. As far as alternatives, why don't we target the .net CLI? j/k! -- Nathan Heagy phone:306.653.4747 fax:306.653.4774 http://www.zu.com

As far as alternatives, why don't we target the .net CLI?
http://www.msdn.microsoft.com/library/default.asp?url=/library/en-us/cpguide... ml/cpconcompilationreuse.asp

Nathan Heagy wrote:
The question I'd really like to ask, which I'll just throw out for now, is: Why would minimal python want to generate bytecode for the old interpreter?
Is byte-code compatibility a goal of minimalPython? I haven't seen it mentioned anywhere but everyone seems to be working under that assumption.
This was an early consideration. We wanted to stay as compatible as possible, especially in the bootstrap phase. It would be nice to use CPython as cross compiler to bytecode, which the new thing can execute then, even before it is able to compile from alone. But there is so much flux in the project right now, that this requirement might become useless, I have no idea yet.
As far as alternatives, why don't we target the .net CLI?
.net at all would deserve an extra thread. So let me answer it for anything different from .pyc code: Python byte code is well known, there is an interpreter called CPython, there is an almost complete compiler in Python, there is Psyco which works right now with it, Bytecodehacks, the dis module and much more. There is more common knowledge about byte code in the heads which are going to join than about any other intermediate language, I guess. If we want to let this sprint produce anything executable which has to deal with an interpreter engine at all, then we should not start from scratch. Other engines can be discussed when we have something that works. ciao - chris

[Jeremy Hilton] The question I'd really like to ask, which I'll just throw out for now, is: Why would minimal python want to generate bytecode for the old interpreter? [Nathan Heagy] Is byte-code compatibility a goal of minimalPython? I haven't seen it mentioned anywhere but everyone seems to be working under that assumption. We are working from the known towards the unknown. It is clear that we want to have a compiler package written in python and if feasible an appropriate parser. Later on we probably want to use it to generate *very* different stuff than the current Python bytecode. [Nathan Heagy] As far as alternatives, why don't we target the .net CLI? Because we don't aim at buzzword compliancy :-) From what i remember about .net/cli discussions it just doesn't fit well with python's dynamics. If you know otherwise then please post more details. regards, holger

Hello Jeremy, On Thu, Jan 23, 2003 at 11:20:35AM -0500, Jeremy Hylton wrote:
I believe the stack depth computation is still pretty bogus, although Mark Hammond did a good job of getting it mostly correct.
I suddenly realize that I've got a toy that can be modified in 10 minutes to compute the stack depth with very little chance of a complex bug sneaking in. (Sorry, I don't have it right under my hand.) The toy is the main loop of a Python interpreter in Python (which some of us already worked on for fun, with a Frame class and with a method for each of the opcodes). In other words, we (the PyPython project) don't need to worry abot computing the stack depth with data flow analysis or whatever because our project will automatically produce such a tool for free! That's "abstract interpretation". To explain what I have in mind, let me first describe the PyPython main loop. First let me insist again on the separation between interpreter-level objects and application-level objects. Imagine that the stack in the interpreter is maintained as a list, and that at some point in the interpretation the stack contains the integers 1, 2 and 3. Then the stack list must *not* be the list [1, 2, 3]. That's the point I already discussed: it must be a list of three *complex objects* that represent the application-level integers 1, 2 and 3. For example, it could be [PyInt(1), PyInt(2), PyInt(3)], where PyInt is one of our custom classes. To show the confusion that would araise from a stack that would really contain [1, 2, 3], try to see how the interpreter should implement type()! You cannot. But it is trivial to add an ob_type() method to the PyInt class. Now to the point. If you take the same Python code that implements the main loop and all the opcodes, but you *replace* PyInt, PyStr, etc. with a trivial empty class Placeholder, you get an abstract interpreter. Running it against a real code object, you will see the stack evolve with no real objects in it. For example, at the point where the stack could have hold [PyInt(1), PyInt(2), PyInt(3)], now it is just [Placeholder(), Placeholder(), Placeholder()]. So what we need to do to compute the stack depth is interpret the code object once. On conditional jumps, we "fork", i.e. we try both paths. When we jump back to an already-seen position, we "merge", i.e. we keep the longest of the previous and the current stack for that position. When this process finishes, we know exactly how long the stack can be at each position. Note that the same technique can be extended to make a bytecode checker that can prove that an arbitrary bytecode is correct, in the sense that it will not crash the interpreter. This is what I was trying to do in my toy code. What's interesting here is that there seems to be a general pattern about re-using the Python-in-Python main loop. Psyco also works like this, only merging is more subtle. In all the cases (regular interpretation, stack depth computation, bytecode checking, Psyco) the PyPython main loop is *exactly* the same; the things that change are: * the implementation of PyInt, PyStr, etc. For regular interpretation it is implemented with a real int, str, etc. object. For stack depth it is a dummy placeholder. For Psyco it is a more complex structure that can tell apart run-time and compile-time values. * how we do forking. The (generic) JUMP_IF_TRUE opcode could be written with something like: def JUMP_IF_TRUE(self, arg): # self=frame, arg=offset to jump target v = self.tos() # top-of-stack object if v.istrue(): self.next_instr += arg # jump forward by 'arg' The meaning is clear in the case of regular interpretation. For stack depth computation, the istrue() method does some magic: it forks the current frame and returns twice to its caller, once returning 'false' so that JUMP_IF_TRUE() will continue to inspect the 'false' branch, and later returning 'true' to inspect the other branch. Yes, returning twice from a call is what *continuations* are for. No, Python has no such notion. Yes, Stackless would be very handy there. No, I'm not suggesting that we are stuck to enhancing CPython or switching to an altogether different language that has continuations. PyPython core should at some place be translated to other languages automatically; this translation can write continuation-passing-style functions if needed (in C or again in Python!). * finally, merging is important in all but the regular interpretation case, to detect when we reached a point that we have already seen. That's some action that must be done at regular interval, between two opcodes. So if PyPython contains a "poll()" call where CPython contains the regular-interval checks, we can implement poll() to do whatever is required for the intended usage of PyPython, e.g. poll the Unix signals for regular interpretation, or merge for the stack depth computer. I hope you are convinced now that the interpreter stack must not be [1, 2, 3] :-) A bientôt, Armin.
participants (9)
-
Armin Rigo
-
Christian Tismer
-
Guido van Rossum
-
holger krekel
-
Jeremy Hylton
-
logistix
-
Michael Hudson
-
Nathan Heagy
-
Samuele Pedroni