Turtle Interpreter

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

ตอนแรกที่เห็นโจทย์นี้ก็คิดหนัก เพราะ ruby ยังไม่มี parser library ที่ถูกใจเลย
ร่ำๆจะไปใช้ python เขียนแล้ว
แต่ก็ไป search หาเจอเสียก่อน
library ตัวนี้มีชื่อว่า dhaka

เริ่มแรกสุดก็คือการเขียน Grammar rule เพื่อที่จะนำไป generate parser

require 'rubygems'
require 'dhaka'
 
class TurtleGrammar < Dhaka::Grammar
 
  for_symbol(Dhaka::START_SYMBOL_NAME) do
    program            %w| Statement_List |
  end
 
  for_symbol("Statement_List") do
    single_statment    %w| Statement |
    multiple_statement %w| Statement_List Statement |
  end
 
  for_symbol("Statement") do
    draw_forward       %w| DRAW_FORWARD Argument |
    move_forward       %w| MOVE_FORWARD Argument |
    rotate             %w| ROTATE Direction Argument |
    save_state         %w| SAVE_STATE |
    recover_state      %w| RECOVER_STATE |
    repeat_statement   %w| REPEAT Argument Statement_List END_REPEAT |
    variable_declaration %w| VAR literal Argument |
    variable_operation %w| Operation |  
  end
 
  for_symbol("Operation") do
    mul_op             %w| MUL literal Argument |
    add_op             %w| ADD literal Argument |
    div_op             %w| DIV literal Argument |
    sub_op             %w| SUB literal Argument |
  end
 
  for_symbol("Argument") do
    identifier         %w| literal |
    integer            %w| numeric_literal |
  end
 
  for_symbol("Direction") do
    clockwise          %w| CLOCKWISE |
    counterclockwise   %w| COUNTERCLOCKWISE |
  end
end

Note: จะเห็นว่า เขาพยายามทำให้มันเป็น Domain Specific Language (DSL)

Input ที่จะ feed เข้า Parser ก็คือ Token ซึ่งถูกสร้างโดย Lexer
ใน Dhaka เราสามารถเขียน Lexer Specification แล้วให้ Dhaka ช่วย generate Lexer ให้

class TurtleLexerSpecification < Dhaka::LexerSpecification
 
  KEYWORDS = %w| DRAW_FORWARD MOVE_FORWARD 
                 ROTATE SAVE_STATE RECOVER_STATE
                 CLOCKWISE COUNTERCLOCKWISE
                 REPEAT END_REPEAT 
                 VAR MUL ADD SUB DIV
               |
 
  for_pattern("\n") do 
    # ignore newline
  end
 
  for_pattern(' ') do
    # ignore whitespace
  end
 
  for_pattern('(\w|_)+') do
    if KEYWORDS.include? current_lexeme.value
      create_token current_lexeme.value
    else
      create_token 'literal'
    end
  end
 
  for_pattern('\d*') do
    create_token('numeric_literal')
  end
 
end

เมื่อได้ spec ของทั้ง Lexer และ Grammar แล้ว
เราก็ทำการ สร้าง Lexer และ Parser file
ผม implement script สำหรับ build โดยใช้ Rake (build tool ของ ruby)

task :compile => [:generate_parser, :generate_lexer]
 
task :generate_parser do
  require 'rubygems'
  require 'dhaka'
  require 'turtle_grammar'
  File.open('turtle_parser.rb','w') do |file| 
    file << "require 'rubygems'\n"
    file << "require 'dhaka'\n"
    file << "require 'turtle_grammar'\n"
 
    parser = Dhaka::Parser.new(TurtleGrammar)
    file << parser.compile_to_ruby_source_as(:TurtleParser)
  end
end
 
task :generate_lexer do
  require 'rubygems'
  require 'dhaka'
  require 'turtle_lexer_specification'
 
  File.open('turtle_lexer.rb', 'w') do |file| 
    file << "require 'rubygems'\n"
    file << "require 'dhaka'\n"
    file << "require 'turtle_lexer_specification'\n"
    file << Dhaka::Lexer.new(TurtleLexerSpecification).compile_to_ruby_source_as(:TurtleLexer)
  end
end

การทำงาน หลังจากที่ parser ได้รับ feed จาก lexer แล้ว
parser ก็จะสร้าง Abstract Syntax Tree ขึ้นมา
ซึ่งสามารถนำไปใช้ในการ evaluate ได้โดยการ traverse ไปตาม node ต่างๆ

ใน dhaka มี class ที่ช่วยในการท่อง tree ด้วย
โดยเราต้อง extend class Evaluator

require 'rubygems'
require 'turtle_grammar'
 
class TurtleEvaluator < Dhaka::Evaluator
  self.grammar = TurtleGrammar
 
  def initialize(callback)
    @vars = {}
    @states = []
    @state = State.new
    @callback = callback
  end
 
  define_evaluation_rules do
 
    for_multiple_statement do
      evaluate(child_nodes[0])
      evaluate(child_nodes[1])
    end
 
    for_draw_forward do
      line = @state.forward(evaluate(child_nodes[1]))
      @callback.addLine(*line)
    end
 
    for_move_forward do
      @state.forward(evaluate(child_nodes[1]))
    end
 
    for_rotate do
      dir = evaluate(child_nodes[1])
      angle = evaluate(child_nodes[2])
      @state.rotate(dir,angle)
    end
 
    for_clockwise do
      "CLOCKWISE"
    end
 
    for_counterclockwise do
      "COUNTERCLOCKWISE"
    end
 
    for_save_state do
      @states.push(@state.clone)
    end
 
    for_recover_state do
      @state = @states.pop
    end
 
    for_repeat_statement do
      cnt = evaluate(child_nodes[1])
      cnt.times do
        evaluate(child_nodes[2])
      end
    end
 
    for_variable_declaration do
      varname = child_nodes[1].token.value
      value = evaluate(child_nodes[2])
      @vars[varname] = value
    end
 
    for_mul_op do
      name = child_nodes[1].token.value
      @vars[name] = @vars[name] * evaluate(child_nodes[2])
    end
 
    for_add_op do
      name = child_nodes[1].token.value
      @vars[name] = @vars[name] + evaluate(child_nodes[2])
    end
 
    for_div_op do
      name = child_nodes[1].token.value
      @vars[name] = @vars[name] / evaluate(child_nodes[2])
    end
 
    for_sub_op do
      name = child_nodes[1].token.value
      @vars[name] = @vars[name] - evaluate(child_nodes[2])
    end
 
    for_identifier do
      name = child_nodes[0].token.value
      @vars[name]
    end
 
    for_integer do      
      child_nodes[0].token.value.to_i
    end  
 
  end
 
end

จะเห็นว่า evaluator ไม่ทำอะไรมาก
ส่วนที่เกี่ยวกับ สถานะ ก็จะ delegate ให้ Object State
ส่วนที่เกี่ยวกับ การ Plot ก็จะเรียกใช้ callback function
หน้าตาของ State เป็นดังนี้

class State
  attr_accessor :alpha, :x, :y
 
  def initialize
    @x = 150
    @y = 300
    @alpha = 90 # north
  end
 
  def forward(value)
    oldx = @x
    oldy = @y
    radian = Math::PI * (90+@alpha) / 180
    @x += value * (Math.sin(radian))
    @y += value * (Math.cos(radian))
    [oldx,oldy,@x,@y]
  end
 
  def rotate(dir,angle)
    if dir == 'CLOCKWISE'
      @alpha -= angle
    else
      @alpha += angle
    end
  end
end

สุดท้ายก็จบด้วยส่วน open-gl ที่ทำหน้าที่ Render
และใช้เป็น callback pass ไปให้ Evaluator

require 'rubygems'
require 'gl'
require 'glut'
 
class Canvas
 
  def initialize
    @points = []
  end
 
  def addLine(x,y,x1,y1)
    puts "#{x},#{y} -> #{x1},#{y1}"
    @points << [x,y,x1,y1]
  end
 
 
  def run() 
    STDOUT.sync = TRUE
    Glut.glutInit
    Glut.glutInitWindowSize(300, 300);
    a = Glut.glutCreateWindow("Turtle")
    Glut.glutDisplayFunc(disp)
    Glut.glutReshapeFunc(reshape);
    Glut.glutMainLoop;
  end
 
  def reshape() 
    Proc.new {|w, h|
      Gl.glViewport(0, 0, w, h)
      Gl.glMatrixMode(Gl::GL_PROJECTION)
      Gl.glLoadIdentity
      Gl.glOrtho(0, w, 0, h, -1, 1)
      Gl.glScale(1, -1, 1)
      Gl.glTranslate(0, -h, 0)
    }
  end
 
  def disp()
    Proc.new {
      Gl.glClear(Gl::GL_COLOR_BUFFER_BIT)     
      Gl.glBegin(Gl::GL_LINES)
      Gl.glColor(1,1,1)
      @points.each do |point|
        Gl.glVertex(point[0],point[1])
        Gl.glVertex(point[2],point[3])
      end
      Gl.glEnd
      Gl.glFlush      
    }
  end
 
end

เวลา run ก็ประมาณนี้

require 'rubygems'
require 'dhaka'
require 'turtle_lexer_specification'
require 'turtle_parser'
require 'turtle_lexer'
require 'turtle_evaluator'
 
require 'canvas'
 
canvas = Canvas.new
 
 
code = STDIN.read
parse_result = TurtleParser.parse(TurtleLexer.lex(code))
case parse_result
when Dhaka::TokenizerErrorResult
  puts 'tokenize Error' 
  puts parse_result.unexpected_char_index
  exit(2)
when Dhaka::ParseErrorResult
  puts 'parse error'
  puts parse_result.unexpected_token
  exit(1)
end
evaluator = TurtleEvaluator.new(canvas)
evaluator.evaluate(parse_result)
 
canvas.run
sugree's picture

python มี parser ตัวไหนถูกใจเหรอครับ ผมว่า dhaka ดูดีมากแล้วนะเนี่ย

เคยใช้ PLY ซึ่งผมก็ ok กับ feature แล้วครับ.
(ตอนนี้ก็ว่า code PLY มันสวยแล้วนะ
แต่ตอนนี้ grammar ชอง dhaka สวยกว่าหน่อย)

ข่าวร้าย
run โปรแกรมที่ยาวมากๆ (ที่ gen จาก lindenmayer ที่ depth สูงๆ)
เกิด stack overflow ใน dhaka
ยังขี้เกี่ยจดู แต่เกิดจาก recursive ลึกเกิน stack
ในส่วนของ Grammar นี้
multiple_statement %w| Statement_List Statement |

ย้าย Codenone

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

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