# Turtle drawing program in Python # Author: Jeff Terrell # Date: Thu, 2008-04-10 from Tkinter import * class Token: def __init__(self, t, n): self.type = t self.name = n def tostr(self): return "[" + self.type + " | '" + self.name + "']" import sys import re def scan(filename): try: filein = open(filename) except IOError, (errno, strerror): print "I/O error(%s): %s" % (errno, strerror) sys.exit() # a simple approach: read the entire file in as a single string cstream = filein.read() filein.close() tokens = [] last_len = len(cstream) # The basic scanning approach is to determine what token is at the head of # the string, strip it off, and add it to the list of tokens. Loop until # we exhaust the input. while len(cstream) > 0: # Strip off whitespace r = re.compile(r"^\s*(//.*)?", re.MULTILINE) cstream = r.sub("", cstream) if len(cstream) == 0: tokens.append(Token('EOF', '')) # keywords: r = re.compile(r'DRAW_FORWARD\b') m = r.match(cstream) if m: cstream = cstream[12:] tokens.append(Token('DRAW','')) r = re.compile(r'MOVE_FORWARD\b') m = r.match(cstream) if m: cstream = cstream[12:] tokens.append(Token('MOVE','')) r = re.compile(r'ROTATE\b') m = r.match(cstream) if m: cstream = cstream[6:] tokens.append(Token('ROTATE','')) r = re.compile(r'SAVE_STATE\b') m = r.match(cstream) if m: cstream = cstream[10:] tokens.append(Token('SAVE','')) r = re.compile(r'RECOVER_STATE\b') m = r.match(cstream) if m: cstream = cstream[13:] tokens.append(Token('RECOVER','')) r = re.compile(r'CLOCKWISE\b') m = r.match(cstream) if m: cstream = cstream[9:] tokens.append(Token('CLOCKWISE','')) r = re.compile(r'COUNTERCLOCKWISE\b') m = r.match(cstream) if m: cstream = cstream[16:] tokens.append(Token('COUNTERCLOCKWISE','')) r = re.compile(r'REPEAT\b') m = r.match(cstream) if m: cstream = cstream[6:] tokens.append(Token('REPEAT','')) r = re.compile(r'END_REPEAT\b') m = r.match(cstream) if m: cstream = cstream[10:] tokens.append(Token('END','')) r = re.compile(r'VAR\b') m = r.match(cstream) if m: cstream = cstream[3:] tokens.append(Token('VAR','')) r = re.compile(r'MUL\b') m = r.match(cstream) if m: cstream = cstream[3:] tokens.append(Token('MUL','')) r = re.compile(r'ADD\b') m = r.match(cstream) if m: cstream = cstream[3:] tokens.append(Token('ADD','')) r = re.compile(r'SUB\b') m = r.match(cstream) if m: cstream = cstream[3:] tokens.append(Token('SUB','')) r = re.compile(r'DIV\b') m = r.match(cstream) if m: cstream = cstream[3:] tokens.append(Token('DIV','')) # Identifiers r = re.compile(r'[a-zA-Z][a-zA-Z0-9_]*') m = r.match(cstream) if m: id = m.group(0) cstream = cstream[len(id):] tokens.append(Token('ID',id)) # Integers r = re.compile(r'[+-]?\d+') m = r.match(cstream) if m: num = m.group(0) cstream = cstream[len(num):] tokens.append(Token('INT',num)) # If there's an invalid token at the head of the character stream, # we'll keep looping, over and over, never advancing the stream. So, # we test the length. If it hasn't changed since the last iteration, # we have an invalid token. if last_len == len(cstream): sys.stderr.write('Error: invalid token at head of cstream.\ncstream is:\n"""\n%s\n"""\n' % cstream) sys.exit(1) last_len = len(cstream) return tokens # Turtle++ grammar: """ program := stmt_list EOF stmt_list := stmt stmt_list | e stmt := action | repeat_stmt | decl | operation action := DRAW arg | MOVE arg | ROTATE dir arg | SAVE | RECOVER dir := CLOCKWISE | COUNTERCLOCKWISE arg := ID | INT repeat_stmt := REPEAT arg stmt_list END decl := VAR ID arg operation := MUL ID arg | ADD ID arg | SUB ID arg | DIV ID arg """ global cur_tok global tok_list import traceback def match(type): global cur_tok global tok_list if type == cur_tok.type: saved = cur_tok; if len(tok_list) > 0: cur_tok = tok_list.pop(0) else: cur_tok = Token('EOF', '') return saved sys.stderr.write('Error: expected %s but got %s\n\n' % (type, cur_tok.type)) sys.stderr.write('Token list is: {{{\n\t%s\n' % cur_tok.tostr()) for t in tok_list: sys.stderr.write('\t%s\n' % t.tostr()) sys.stderr.write('}}}\n\n') sys.stderr.write(traceback.print_tb(sys.exc_info()[2])) sys.exit() def check(type): global cur_tok return type == cur_tok.type # program := stmt_list EOF def program(): return ['stmt_list', stmt_list(), match('EOF')] # stmt_list := stmt stmt_list | e def stmt_list(): if stmtPending(): return ['stmt_list', stmt(), stmt_list()] return [] # stmt := action | repeat_stmt | decl | operation def stmt(): if actionPending(): return ['stmt', action()] if repeat_stmtPending(): return ['stmt', repeat_stmt()] if declPending(): return ['stmt', decl()] return ['stmt', operation()] def stmtPending(): return actionPending() or repeat_stmtPending() or \ declPending() or operationPending() # action := DRAW arg | MOVE arg | ROTATE dir arg | SAVE | RECOVER def action(): if check('DRAW'): return ['action', match('DRAW'), arg()] if check('MOVE'): return ['action', match('MOVE'), arg()] if check('ROTATE'): return ['action', match('ROTATE'), dir(), arg()] if check('SAVE'): return ['action', match('SAVE')] return ['action', match('RECOVER')] def actionPending(): return check('DRAW') or check('MOVE') or check('ROTATE') or \ check('SAVE') or check('RECOVER') # dir := CLOCKWISE | COUNTERCLOCKWISE def dir(): if check('CLOCKWISE'): return ['dir', match('CLOCKWISE')] return ['dir', match('COUNTERCLOCKWISE')] # arg := ID | INT def arg(): if check('ID'): return ['arg', match('ID')] return ['arg', match('INT')] # repeat_stmt := REPEAT arg stmt_list END def repeat_stmt(): return ['repeat_stmt', match('REPEAT'), arg(), stmt_list(), match('END')] def repeat_stmtPending(): return check('REPEAT') # decl := VAR ID arg def decl(): return ['decl', match('VAR'), match('ID'), arg()] def declPending(): return check('VAR') # operation := MUL ID arg | ADD ID arg | SUB ID arg | DIV ID arg def operation(): if check('MUL'): return ['operation', match('MUL'), match('ID'), arg()] if check('ADD'): return ['operation', match('ADD'), match('ID'), arg()] if check('SUB'): return ['operation', match('SUB'), match('ID'), arg()] return ['operation', match('DIV'), match('ID'), arg()] def operationPending(): return check('MUL') or check('ADD') or check('SUB') or check('DIV') def parse(toks): global cur_tok global tok_list tok_list = toks cur_tok = tok_list.pop(0) return ['program', program()] import types def printParseTree(p, tabstop=0): if type(p) is types.ListType: if len(p) == 0: print '. '*tabstop + 'e' return print '. '*tabstop + p[0] for el in p[1:]: printParseTree(el, tabstop+1) elif isinstance(p, Token): s = p.tostr() print '. '*tabstop + p.tostr() import copy from math import * global namespace global ns_stack def evalProgram(p): global namespace global ns_stack namespace = dict(angle=90, x=150, y=300, vars={}) ns_stack = [] eval(p) def eval(p): if len(p) < 1: return if p[0] == "program": # eval(p[1]) eval(p[1][1]) elif p[0] == "stmt_list": if len(p) > 1: eval(p[1]) eval(p[2]) elif p[0] == "stmt": eval(p[1]) elif p[0] == "action": evalAction(p[1:]) elif p[0] == "dir": return p[1].type elif p[0] == "arg": return evalArg(p[1]) elif p[0] == "repeat_stmt": a = evalArg(p[2]) for i in range(a): eval(p[3]) elif p[0] == "decl": evalDecl(p[2].name, p[3]) elif p[0] == "operation": evalOperation(p[1:]) else: print "error: %s not an evaluable type" % printParseTree(p) def evalAction(a): if a[0].type == "DRAW": val = evalArg(a[1]) evalDraw(val) elif a[0].type == "MOVE": moveForward(evalArg(a[1])) elif a[0].type == "ROTATE": evalRotate(eval(a[1]), evalArg(a[2])) elif a[0].type == "SAVE": evalSave() elif a[0].type == "RECOVER": evalRestore() else: print "error: %s not an action" % repr(a) def evalArg(a): if a[0].type == "ID": if inNamespace(a[0].name): return lookupVar(a[0].name) elif a[0].type == "INT": return int(a[0].name) else: print "error: %s not an arg" % repr(a) def evalOperation(o): a = evalArg(o[2]) if o[0].type == "MUL": evalMul(o[1].name, a) elif o[0].type == "ADD": evalAdd(o[1].name, a) elif o[0].type == "SUB": evalSub(o[1].name, a) elif o[0].type == "DIV": evalDiv(o[1].name, a) else: print "error: %s not an operation" % repr(o) def evalDecl(n,v): saveVar(n, evalArg(v)) def evalDraw(val): drawLine(val) moveForward(val) def evalMove(val): moveForward(val) def evalRotate(dir, val): changeAngle(dir, val) def evalSave(): pushNamespace() def evalRestore(): popNamespace() def evalMul(n,v): saveVar(n, lookupVar(n)*v) def evalAdd(n,v): saveVar(n, lookupVar(n)+v) def evalSub(n,v): saveVar(n, lookupVar(n)-v) def evalDiv(n,v): saveVar(n, lookupVar(n)/v) def evalArg(a): name = a[1].name if inNamespace(name): return lookupVar(name) return int(name) def drawLine(val): global namespace global canvas x2 = namespace['x'] + val*cos(namespace['angle']*pi/180) y2 = namespace['y'] - val*sin(namespace['angle']*pi/180) #print "drawing line from (%d,%d) to (%d,%d)" % (namespace['x'], namespace['y'], x2, y2) canvas.create_line(namespace['x'], namespace['y'], x2, y2) def moveForward(val): global namespace namespace['x'] += val*cos(namespace['angle']*pi/180) namespace['y'] -= val*sin(namespace['angle']*pi/180) #print "moving %d pixels forward to (%d,%d)" % (val, namespace['x'], namespace['y']) def changeAngle(dir, val): global namespace #print "rotating %d degrees %s" % (val, dir) if dir == 'CLOCKWISE': val = -val; namespace['angle'] += val namespace['angle'] = namespace['angle'] % 360 def inNamespace(name): global namespace return name in namespace['vars'] def saveVar(name, val): global namespace namespace['vars'][name] = val def lookupVar(name): global namespace return namespace['vars'][name] def pushNamespace(): global namespace global ns_stack # the deepcopy is important! ns_stack.append(copy.deepcopy(namespace)) def popNamespace(): global namespace global ns_stack if len(ns_stack) == 0: sys.stderr.write("Error: no namespace to restore\n") sys.exit() namespace = ns_stack.pop() # A line-drawing example def say_hello(): global canvas # H canvas.create_line(10,50,10,250) canvas.create_line(50,50,50,250) canvas.create_line(10,150,50,150) # E canvas.create_line(60,50,60,250) canvas.create_line(60,50,100,50) canvas.create_line(60,150,100,150) canvas.create_line(60,250,100,250) # L canvas.create_line(110,50,110,250) canvas.create_line(110,250,150,250) # L canvas.create_line(160,50,160,250) canvas.create_line(160,250,200,250) # O canvas.create_oval(210,50,250,250) # ! canvas.create_line(270,50,270,235) canvas.create_oval(265,245,275,255) def main(argv=None): global canvas # Initialize the graphical toolkit root = Tk() canvas = Canvas(root, width=300, height=300) canvas.pack(side=TOP) #say_hello() # parse the Turtle input if argv is None: argv = sys.argv if len(argv) != 2: sys.stderr.write('Error: specify the filename to read as input\n') sys.exit() else: #print 'parsing %s' % argv[1] p = parse(scan(argv[1])) evalProgram(p) #printParseTree(p) # Display the canvas # This must come last, after all line-drawing commands root.mainloop() global canvas # So, main() will only get called if this program is run directly, but not when # it's imported somewhere else. Nifty, eh? if __name__ == "__main__": main()