07/09/2018, 16:05

Ruby Internal - Code Ruby của bạn được thực thi như thế nào (Phần 1)

Bài viết nằm trong chuỗi hard-core là một group học nhóm lập ra bởi một số thành viên của ruby VN. Rule của nhóm nằm tại đây Idea là mỗi week thành viên sẽ pick ra một topic và sau 1 tuần sẽ phải có output về topic đó. Các ví dụ trong chuỗi bài viết này chủ yếu được lấy từ cuốn "Ruby Under a ...

Bài viết nằm trong chuỗi hard-core là một group học nhóm lập ra bởi một số thành viên của ruby VN. Rule của nhóm nằm tại đây Idea là mỗi week thành viên sẽ pick ra một topic và sau 1 tuần sẽ phải có output về topic đó.

Các ví dụ trong chuỗi bài viết này chủ yếu được lấy từ cuốn "Ruby Under a Microscope" của tác giả Pat Shaughnessy

Là một Rubyist, đã bao giờ bạn tự hỏi bản thân mình rằng, một đoạn Ruby code như bên dưới được thực thi như thế nào không?

puts 2 + 3
# => 5

Rằng đoạn code trên được Ruby đọc và chuyển hóa bao nhiêu lần trước khi nó được thực thi?

Chính xác là ba lần. Dù bạn chạy một đoạn code siêu đơn giản như trên, một Rails app vĩ đại hay một rake task, code của bạn đều được Ruby tách thành những phần nhỏ (tí hon) và chuyển hóa thành những format khác nhau tổng cộng ba lần. Khoảng thời gian từ khi bạn gõ lệnh ruby -e "puts 2 + 3" đến khi bạn thấy số 5 được hiện ra trên console, đó thật sự là quá trình dài mà rất nhiều kĩ thuật, công nghệ và thuật toán được dùng đến.

alt text

  1. Đầu tiên Ruby tokenize đoạn code của bạn thành những token.
  2. Tiếp theo nó parse những token đó thành Abstract Syntax Tree (AST) Node
  3. Sau đó compile (biên dịch) thành bytecodes là một tập các lệnh thực thi cấp thấp (tuy nhiên không phải là mã máy). Đây chính là tập lệnh được chạy trong máy áo Ruby (Ruby Virtual Machine).

Thuật toán tokenize

Ở chương này mình sẽ nói kĩ về phần tokenizing.

Giả sử bạn có một đoạn Ruby code sau

# simple.rb
10.times do |n|
  puts n
end
$ ruby simple.rb

Đầu tiên Ruby sẽ đọc file simple.rb và split nó thành những kí tự.

[1] [0] [.] [t] [i] [m] [e] [s] [ ] [d] [o] [|] [n] [|]

Ta có một con trỏ đọc từng kí tự của hàng đầu tiên.
Ruby nhận ra rằng số 1 là bắt đầu của một số, nó sẽ tiếp tục đọc cho đến khi trỏ đến một kí tự không phải số.

 *
[1] [0] [.] [t] [i] [m] [e] [s] [ ] [d] [o] [|] [n] [|]

Tiếp theo con trỏ đến số 0, vẫn là một con số, nên nó nhảy đến kí tự tiếp theo

     *
[1] [0] [.] [t] [i] [m] [e] [s] [ ] [d] [o] [|] [n] [|]

Ruby nhận ra rằng . vẫn có thể là một phần của số thực và nhảy đến kí tự tiếp theo.

         *
[1] [0] [.] [t] [i] [m] [e] [s] [ ] [d] [o] [|] [n] [|]

Đến khi gặp kí tự t, đây không phải là một phần của một con số, đồng thời kết luận không còn con số nào sau dấu . lúc nãy nữa, Ruby nhận thấy rằng dấu . đó là thể là một phần của token khác, nên trỏ ngược về thêm một kí tự.

             *
[1] [0] [.] [t] [i] [m] [e] [s] [ ] [d] [o] [|] [n] [|]
         *
[1] [0] [.] [t] [i] [m] [e] [s] [ ] [d] [o] [|] [n] [|]

Lúc này Ruby sẽ convert những kí tự số nó đã đi qua thành (tINTERGER) token.

            *
(tINTEGER) [.] [t] [i] [m] [e] [s] [ ] [d] [o] [|] [n] [|]

Ruby trỏ tiếp đến kí tự tiếp theo và convert kí tự nó vừa đi qua dấu . thành token (.)

                *
(tINTEGER) (.) [t] [i] [m] [e] [s] [ ] [d] [o] [|] [n] [|]

Và như thế Ruby lướt qua times, và convert nó thành token (tIDENTIFIER)

                              *
(tINTEGER) (.) (tIDENTIFIER) [d] [o] [|] [n] [|]

Note: IDENTIFER không phải là reserved keyword (từ khóa) trong Ruby, nó dùng để chỉ một biến (var), một hàm (function), hay một method.

Tiếp theo Ruby đọc qua do và nhận ra đây là reserved keyword (keyword_do)

                                           *
(tINTEGER) (.) (tIDENTIFIER) (keyword_do) [|] [n] [|]

Và rồi, Ruby cũng token xong hàng thứ nhất của đoạn code Ruby của chúng ta.

                                           *
(tINTEGER) (.) (tIDENTIFIER) (keyword_do) (|) (tIDENTIFIER) (|)

Ripper

Chúng ta đã hiểu ý tưởng cơ bản của việc tokenize. Để vọc thêm về Ruby tokenizer, ta có thể dùng tool Ripper để kiểm tra toàn bộ token được sinh ra của một đoạn Ruby code.

require 'ripper'
require 'pp'
code = <<STR
10.times do |n|
 puts n
end
STR
puts code
pp Ripper.lex(code)
/*
[[[1, 0], :on_int, "10"],
[[1, 2], :on_period, "."],
[[1, 3], :on_ident, "times"],
[[1, 8], :on_sp, " "],
[[1, 9], :on_kw, "do"],
[[1, 11], :on_sp, " "],
[[1, 12], :on_op, "|"],
[[1, 13], :on_ident, "n"],
[[1, 14], :on_op, "|"],
[[1, 15], :on_ignored_nl, "
"],
[[2, 0], :on_sp, " "],
[[2, 2], :on_ident, "puts"],
[[2, 6], :on_sp, " "],
[[2, 7], :on_ident, "n"],
[[2, 8], :on_nl, "
"],
[[3, 0], :on_kw, "end"],
[[3, 3], :on_nl, "
"]]
*/

Như ta thấy, các token được sinh ra ở đoạn code trên là:

  • 10 = INT
  • times, n = ident
  • do, end = keyword

Ta đã hoàn thành phần 1 - Tokenizer trong quá trình thực thi của code Ruby. Ở phần tiếp theo mình sẽ nói về phần Parsing, quá trình mà Ruby sử những token được sinh ra trong phần Tokenizer này, gộp nó lại thành những cú pháp có ý nghĩa với Ruby.

Ruby Under a Microscope - Pat Shaughnessy

0