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