ยิ่งรู้จัก tools ยิ่งตัดสินใจดีในฐานะ developer โดยเฉพาะ debug performance issues เข้าใจว่า Ruby ทำอะไรตอนรันโปรแกรมสำคัญมาก
โพสต์นี้ติดตาม journey ของโปรแกรมง่ายๆ ตอน lexed, parsed, compiled เป็น bytecode ใช้ tools ที่ Ruby ให้ spy interpreter ทุกขั้นตอน
别担心 — แม้ไม่ใช่ expert โพสต์นี้ follow ได้ง่าย มากกว่า guided tour ไม่ใช่ technical manual
เจอ Sample Program
ตัวอย่างใช้ if/else statement เดียว ประหยัดพื้นที่เขียน ternary operator แต่นี่แค่ if/else
x > 100 ? 'foo' : 'bar'
จะเห็นว่าแม้โปรแกรม simple แปลงเป็น data เยอะมากตอน processed
หมายเหตุ: ตัวอย่างทั้งหมด Ruby (MRI) 2.2 implementations อื่นอาจไม่ทำงาน
Tokenizing
ก่อน interpreter รัน ต้องแปลงจาก free-form programming language เป็น structured data มากขึ้น
ขั้นแรกอาจ break โปรแกรมเป็น chunks chunks เรียกว่า tokens
# This is a string
"x > 1"
# These are tokens
["x", ">", "1"]
Ruby standard library มี module Ripper ให้ process Ruby code เหมือน interpreter
ตัวอย่าง用 tokenize method บน Ruby code คืน array of tokens:
require 'ripper'
Ripper.tokenize("x > 1 ? 'foo' : 'bar'")
# => ["x", " ", ">", " ", "1", " ", "?", " ", "'", "foo", "'", " ", ":", " ", "'", "bar", "'"]
Tokenizer ดูดครับ ให้ invalid Ruby ก็ยัง tokenize ได้
# bad code
Ripper.tokenize("1var @= \/foobar`")
# => ["1", "var"]
Lexing
Lexing advanced จาก tokenization string ยังแตก tokens แต่เพิ่ม data ให้ tokens
ตัวอย่าง用 Ripper lex โปรแกรมเล็ก tagging tokens: identifier :on_ident, operator :on_op, integer :on_int ฯลฯ
require 'ripper'
require 'pp'
pp Ripper.lex("x > 100 ? 'foo' : 'bar'")
# [[[1, 0], :on_ident, "x"],
# [[1, 1], :on_sp, " "],
# [[1, 2], :on_op, ">"],
# [[1, 3], :on_sp, " "],
# [[1, 4], :on_int, "100"],
# [[1, 5], :on_sp, " "],
# [[1, 6], :on_op, "?"],
# [[1, 7], :on_sp, " "],
# [[1, 8], :on_tstring_beg, "'"],
# [[1, 9], :on_tstring_content, "foo"],
# [[1, 12], :on_tstring_end, "'"],
# [[1, 13], :on_sp, " "],
# [[1, 14], :on_op, ":"],
# [[1, 15], :on_sp, " "],
# [[1, 16], :on_tstring_beg, "'"],
# [[1, 17], :on_tstring_content, "bar"],
# [[1, 20], :on_tstring_end, "'"]]
ยังไม่มี syntax checking จริง lexer process invalid code ได้
Parsing
ตอน Ruby แตก code เป็น chunks manageable parsing เริ่ม
Parsing stage Ruby transform text เป็น abstract syntax tree (AST) AST คือ representation ของโปรแกรมใน memory
อาจพูดว่า programming languages โดยรวมคือ user-friendly ways 描述 ASTs
require 'ripper'
require 'pp'
pp Ripper.sexp("x > 100 ? 'foo' : 'bar'")
# [:program,
# [[:ifop,
# [:binary, [:vcall, [:@ident, "x", [1, 0]]], :>, [:@int, "100", [1, 4]]],
# [:string_literal, [:string_content, [:@tstring_content, "foo", [1, 11]]]],
# [:string_literal, [:string_content, [:@tstring_content, "foobar", [1, 19]]]]]]
อาจอ่าน output ไม่ง่ายแต่จ้องนานๆ จะเห็น mapping กับโปรแกรมต้นทาง
# Define a progam
[:program,
# Do an "if" operation
[[:ifop,
# Check the conditional (x > 100)
[:binary, [:vcall, [:@ident, "x", [1, 0]]], :>, [:@int, "100", [1, 4]]],
# If true, return "foo"
[:string_literal, [:string_content, [:@tstring_content, "foo", [1, 11]]]],
# If false, return "bar"
[:string_literal, [:string_content, [:@tstring_content, "foobar", [1, 19]]]]]]
จุดนี้ interpreter รู้ทุกอย่างที่คุณอยากให้ทำ รันได้เลย และก่อน Ruby 1.9 จะรัน แต่ตอนนี้มีอีกขั้นตอน
Compiling to bytecode
แทนที่ traverse AST โดยตรง ตอนนี้ Ruby compile AST เป็น lower-level byte code
Byte code รันโดย Ruby virtual machine (YARV)
覗ก inner workings ผ่าน RubyVM::InstructionSequence class ตัวอย่าง compile sample program แล้ว disassemble human readable:
puts RubyVM::InstructionSequence.compile("x > 100 ? 'foo' : 'bar'").disassemble
# == disasm: @>==========
# 0000 trace 1 ( 1)
# 0002 putself
# 0003 opt_send_without_block
# 0005 putobject 100
# 0007 opt_gt , argc:1, ARGS_SIMPLE>
# 0009 branchunless 15
# 0011 putstring "foo"
# 0013 leave
# 0014 pop
# 0015 putstring "bar"
# 0017 leave
Whoa! ดูเหมือน assembly language มากกว่า Ruby มา step through ทำความเข้าใจ:
# เรียก method `x` บน self เก็บผลลัพธ์บน stack
0002 putself
0003 opt_send_without_block
# ใส่เลข 100 บน stack
0005 putobject 100
# ทำ comparison (x > 100)
0007 opt_gt , argc:1, ARGS_SIMPLE>
# ถ้า comparison false ไปบรรทัด 15
0009 branchunless 15
# ถ้า comparison true คืน "foo"
0011 putstring "foo"
0013 leave
0014 pop
# บรรทัด 15 จumped ถ้า comparison false คืน "bar"
0015 putstring "bar"
0017 leave
Ruby virtual machine (YARV) step through instructions execute นั่นแหละ!
Conclusion
จบ tour แบบ simplified, cartoony ของ Ruby interpreter ด้วย tools ที่แสดง guesswork ออกจากการ Ruby interpret programs ไม่ได้ concrete กว่า AST และครั้งหน้าติด performance issue ลองดู bytecode อาจไม่แก้ปัญหาแต่พักใจได้ :)