Turtle Interpreter in Python

  • warning: realpath() [function.realpath]: SAFE MODE Restriction in effect. The script whose uid is 1005 is not allowed to access /tmp owned by uid 0 in /var/www/sites/sugree/codenone.com/subdomains/www/html/includes/file.inc on line 190.
  • warning: realpath() [function.realpath]: SAFE MODE Restriction in effect. The script whose uid is 1005 is not allowed to access /tmp owned by uid 0 in /var/www/sites/sugree/codenone.com/subdomains/www/html/includes/file.inc on line 190.

โจทย์ - http://www.codenone.com/node/242

ตอนแรกกะว่าจะข้ามข้อนี้ไม่เขียน เพราะขี้เกียจ แต่เห็น ruby แล้วอดไม่ได้ สวยจนอยากลอง งานนี้เลยต้องหา lex/yacc สำหรับ Python ดีๆ ซักอัน มองซ้ายมองขวาหาไม่เจอ แต่คลำใน Ubuntu เจอ PLY เลยเอามาใช้ไปก่อน เนื่องจากเห็นมันแปลกดี หน้าตา Python มากๆ

เริ่มแรกด้วยการสร้าง lexer เอาไว้ใช้ตัดคำ วิธีเขียนเป็นแบบตายตัว หน้าตาต้องออกมาแบบนี้เท่านั้น เริ่มจากต้องประกาศชื่อทั้งหมดใน tokens แล้วประกาศชื่อฟังก์ชั่นขื้นต้นด้วย t_ ซึ่งมี regexp ของคำนั้นอยู่ใน docstring ส่วนเนื้อฟังก์ชั่นเอาไว้สำหรับจัดการกับคำนั้น เช่น แปลงสตริงก์เป็นตัวเลข เป็นต้น หรือถ้าไม่มีอะไรพิเศษก็ประกาศเป็นตัวแปรธรรมดาก็ได้ นอกจากนั้นก็จะมี t_ignore เอาไว้สำหรับใส่ regexp ที่ไม่สนใจ หลังจากประกาศทุกอย่างเสร็จก็สร้าง lexer ด้วย lex.lex() จะสังเกตุได้ว่าเขียนแบบนี้ควรใส่ในโมดูลอิสระ แต่จริงๆ ก็ทำเป็นคลาสได้ซึ่งใช้ยากกว่า ยุ่งยากกว่า ถ้าไม่ต้องการอะไรมากก็ไม่จำเป็นต้องใส่ในคลาส ไฟล์นี้ควรชื่อ turtlelex.py

import ply.lex as lex
 
tokens = (
    'NUMERIC_LITERAL',
    'LITERAL',
    'DRAW_FORWARD','MOVE_FORWARD',
    'ROTATE','SAVE_STATE','RECOVER_STATE',
    'CLOCKWISE','COUNTERCLOCKWISE',
    'REPEAT','END_REPEAT',
    'VAR','MUL','ADD','SUB','DIV',
)
 
keywords = ['DRAW_FORWARD','MOVE_FORWARD',
            'ROTATE','SAVE_STATE','RECOVER_STATE',
            'CLOCKWISE','COUNTERCLOCKWISE',
            'REPEAT','END_REPEAT',
            'VAR','MUL','ADD','SUB','DIV']
 
def t_NUMERIC_LITERAL(t):
    r'[+\-]*\d+'
    try:
        t.value = int(t.value)
    except ValueError:
        print 'Line %d: Number %s is too large!' % (t.lineno,t.value)
        t.value = 0
    return t
 
def t_LITERAL(t):
    r'[a-zA-Z_][a-zA-Z0-9_]*'
    if t.value in keywords:
        t.type = t.value
    return t
 
def t_newline(t):
    r'\n+'
    t.lexer.lineno += len(t.value)
 
t_ignore = r' \t'
 
def t_error(t):
    print 'Illegal chararcter "%s"' % t.value[0]
    t.lexer.skip(1)
 
lexer = lex.lex()

ถัดมาคือ grammar generator ซึ่งต้องอยู่ในไฟล์ turtleparse.py. เรื่องน่าสนใจของ yacc ก็อยู่ที่ docstring อีกเช่นเคย ภายใน docstring จะบรรจุกฏเอาไว้ในรูปแบบที่เราๆ คุ้นเคยกันดี โดยเฉพาะในบรรดา RFC ผลของการตัดคำและเรียงคำควรจะได้ออกมาเป็น AST แต่โชคร้าย PLY ไม่มี AST ตอนนี้สมมติก่อนว่ามี ast อยู่แล้ว

import ply.yacc as yacc
 
from turtlelex import tokens
from ast import Node
 
start = 'program'
 
def p_program(p):
    '''program : statement_list'''
    p[0] = Node('program',p[1].children)
 
def p_statement_list(p):
    '''statement_list : statement statement_list
                      | empty'''
    if len(p) == 3:
        children = [p[1]]+p[2].children
    else:
        children = []
    p[0] = Node('statement_list',children)
 
def p_empty(p):
    '''empty :'''
    pass
 
def p_statement(p):
    '''statement : turtle_action
                 | repeat_statement
                 | variable_declaration
                 | variable_operation'''
    p[0] = p[1]
 
def p_turtle_action(p):
    '''turtle_action : DRAW_FORWARD argument
                     | MOVE_FORWARD argument
                     | ROTATE direction argument
                     | SAVE_STATE
                     | RECOVER_STATE'''
    p[0] = Node(p[1],p[2:])
 
def p_direction(p):
    '''direction : CLOCKWISE
                 | COUNTERCLOCKWISE'''
    p[0] = p[1]
 
def p_repeat_statement(p):
    '''repeat_statement : REPEAT argument statement_list END_REPEAT'''
    p[0] = Node(p[1],p[2:4])
 
def p_variable_declaration(p):
    '''variable_declaration : VAR identifier argument'''
    p[0] = Node(p[1],p[2:])
 
def p_variable_operation(p):
    '''variable_operation : MUL identifier argument
                          | ADD identifier argument
                          | DIV identifier argument
                          | SUB identifier argument'''
    p[0] = Node(p[1],p[2:])
 
def p_argument(p):
    '''argument : identifier
                | integer'''
    p[0] = p[1]
 
def p_identifier(p):
    '''identifier : LITERAL'''
    p[0] = Node('identifier',[p[1]])
 
def p_integer(p):
    '''integer : NUMERIC_LITERAL'''
    p[0] = Node('integer',[p[1]])
 
def p_error(p):
    if not p:
        return
    print 'Syntax error at token %s "%s" at line %d' % (p.type,p.value,p.lineno)
 
parser = yacc.yacc()

ได้เวลาสร้าง ast.py ซะที ไหนๆ ก็ไหนๆ เลยใส่คุณปู่ Evaluator เข้าไปด้วย

class Node:
    def __init__(self,operator,children=[]):
        self.operator = operator
        self.children = children
 
    def __repr__(self):
        return '<%s %s>' % (self.operator,self.children)
 
class Evaluator:
    def evaluate(self,node):
        method = getattr(self,'do_%s' % node.operator,None)
        ret = 0
        if not method:
            print 'unknown',node.operator
        else:
            ret = method(*node.children)
        return ret

ซึ่งทำให้เราได้ lex และ yacc ที่สมบูรณ์แล้ว ที่เหลือก็แค่เขียนตัวครอบเพื่อวาดรูป ซึ่งต้องมี

  • TurtleEvaluator สำหรับทำงานตามแต่ AST จะกำหนดไว้
  • State สำหรับเก็บสถานะปัจจุบัน เนื่องจากเต่าตัวนี้มีความจำ
  • TurtleInterpreter เป็นคลาสหลัก

สาระสำคัญอยู่ที่ TurtleEvaluator นี่เอง

#!/usr/bin/env python
 
import math
from Tkinter import *
 
from turtlelex import lexer
from turtleparse import parser
from ast import Evaluator
 
class TurtleEvaluator(Evaluator):
    def __init__(self,callback,state=None):
        self.vars = {}
        self.state = state or State()
        self.states = []
        self.callback = callback
 
    def do_statement_list(self,*stms):
        for stm in stms:
            self.evaluate(stm)
 
    do_program = do_statement_list
 
    def do_integer(self,n):
        return n
 
    def do_DRAW_FORWARD(self,n):
        n = self.evaluate(n)
        line = self.state.forward(n)
        self.callback(*line)
 
    def do_MOVE_FORWARD(self,n):
        n = self.evaluate(n)
        line = self.state.forward(n)
 
    def do_ROTATE(self,dir,angle):
        angle = self.evaluate(angle)
        self.state.rotate(dir,angle)
 
    def do_SAVE_STATE(self):
        self.states.append(self.state.copy())
 
    def do_RECOVER_STATE(self):
        self.state = self.states.pop()
 
    def do_REPEAT(self,count,stm):
        count = self.evaluate(count)
        for i in range(count):
            self.evaluate(stm)
 
    def do_VAR(self,name,n):
        name = name.children[0]
        n = self.evaluate(n)
        self.vars[name] = n
 
    def do_identifier(self,name):
        return self.vars.get(name,0)
 
    def do_ADD(self,name,n):
        name = name.children[0]
        n = self.evaluate(n)
        self.vars[name] = self.vars.get(name,0)+n
 
    def do_MUL(self,name,n):
        name = name.children[0]
        n = self.evaluate(n)
        self.vars[name] = self.vars.get(name,0)*n
 
    def do_SUB(self,name,n):
        name = name.children[0]
        n = self.evaluate(n)
        self.vars[name] = self.vars.get(name,0)-n
 
    def do_DIV(self,name,n):
        name = name.children[0]
        n = self.evaluate(n)
        self.vars[name] = self.vars.get(name,0)/n
 
class State:
    def __init__(self,x=150,y=300,angle=1.0):
        self.x = x
        self.y = y
        self.angle = angle
 
    def copy(self):
        return State(self.x,self.y,self.angle)
 
    def forward(self,value):
        oldx,oldy = self.x,self.y
        radian = math.pi*self.angle
        self.x += value*math.sin(radian)
        self.y += value*math.cos(radian)
        return oldx,oldy,self.x,self.y
 
    def rotate(self,dir,angle):
        if dir == 'CLOCKWISE':
            self.angle -= angle/180.0
        else:
            self.angle += angle/180.0
 
class TurtleInterpreter:
    def __init__(self,parent):
        self.parse_args()
        self.init(parent)
 
    def parse_args(self):
        import sys
        self.infile = sys.argv[1]
        self.width = 300
        self.height = 300
 
    def init(self,parent):
        self.frame = Frame(parent)
        self.frame.pack()
 
        self.canvas = Canvas(self.frame,width=self.width,height=self.height)
        self.canvas.pack(side=TOP)
 
    def run(self):
        input = open(self.infile).read()
        result = parser.parse(input,lexer=lexer)
        ev = TurtleEvaluator(self.add_line,state=State(self.width/2,self.height))
        ev.evaluate(result)
 
    def add_line(self,x0,y0,x1,y1):
        p = self.canvas.create_line(x0,y0,x1,y1)
        self.frame.update()
 
if __name__ == '__main__':
    root = Tk()
    app = TurtleInterpreter(root)
    app.run()
    root.mainloop()

เพื่อความสมบูรณ์มีตัวอย่างโค้ดมาให้ลองด้วย

VAR step 20
MOVE_FORWARD 150
REPEAT 12
    DRAW_FORWARD step
    ROTATE COUNTERCLOCKWISE 90
    DRAW_FORWARD step
    ROTATE COUNTERCLOCKWISE 90
    ADD step 20
END_REPEAT
MOVE_FORWARD 250
ROTATE CLOCKWISE 198
REPEAT 5 DRAW_FORWARD 200 ROTATE COUNTERCLOCKWISE 144 END_REPEAT

อันสุดท้าย เอาไว้ทดสอบการจัดการความผิดพลาด

MOVE_FORWARD 150
A

ดูยังไง Ruby ก็สวยกว่า

ที่ผมเห็นว่า ruby ดูสวยกว่าชัดๆ ก็ตรง Grammar rules นี่แหล่ะครับ
(ตรงอื่นก็ยาวเครือๆกัน)
แต่ผมชอบ PLY มากกว่าตรงที่มันให้เราสร้าง AST เอง
ทำให้รู้สึกว่ายืดหยุ่นกว่า

ส่วนของ dhaka ถ้าอยากได้ AST ที่ไม่ตรงกับ default มัน ก็ต้องเขียน
transform tree เอาเอง
(case ไหนหว่า ที่เราต้องไม่ต้องการใช้ default tree ของ dhaka?)

Note: สำหรับคนยังไม่เคยใช้ PLY
ผมเคยเขียนถึงอยู่หน่อยเหมือนกัน
http://pphetra.blogspot.com/2006/01/plugin-class-diagram-trac-1.html

ย้าย Codenone

ประกาศย้าย Codenone ไปใช้ Forum ของ Blognone แทนครับ ตามไปตั้งกระทู้ต่อได้ที่ Codenone Forum (รายละเอียดอ่านจากกระทู้ ย้าย Codenone ไปรวมกับ Blognone)

กระทู้เก่าๆ จะย้ายตามไปในภายหลัง ตอนนี้ปิดการโพสต์กระทู้ไว้ เหลือไว้เฉพาะอ้างอิงเท่านั้น