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

Cẩm Huỳnh viết ngày 30/08/2016

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 đó.

Lời nói đầu

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).

Tokenizing

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, "\n"],
[[2, 0], :on_sp, " "],
[[2, 2], :on_ident, "puts"],
[[2, 6], :on_sp, " "],
[[2, 7], :on_ident, "n"],
[[2, 8], :on_nl, "\n"],
[[3, 0], :on_kw, "end"],
[[3, 3], :on_nl, "\n"]]
*/

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

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

Tổng kết

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.

Tham khảo:

Ruby Under a Microscope - Pat Shaughnessy

Bình luận


White
{{ comment.user.name }}
Bỏ hay Hay
{{comment.like_count}}
Male avatar
{{ comment_error }}
Hủy
   

Hiển thị thử

Chỉnh sửa

White

Cẩm Huỳnh

45 bài viết.
431 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}
Cùng một tác giả
White
51 5
image cover]imgcover] “Make it work, make it right, make it fast.” Bạn vừa viết xong một ứng dụng web :tada:. Mọi thứ chạy ổn. Code cũng đã được...
Cẩm Huỳnh viết 25 ngày trước
51 5
White
43 9
(Ảnh) Vì sao lại là Bật Đèn? Ai từng đọc qua Tắt Đèn hẳn đã biết tác phẩm được kết thúc bằng tình huống: Buông tay, chị vội choàng dậy, mở cửa...
Cẩm Huỳnh viết hơn 1 năm trước
43 9
White
41 25
Vừa rồi mình vừa tiết kiệm được $5 mỗi tháng sau khi migrate cái (Link) từ Digital Ocean sang Heroku Free Dyno. (Ảnh) Kết quả thật mĩ mãn vì hầu ...
Cẩm Huỳnh viết hơn 1 năm trước
41 25
Bài viết liên quan
White
8 6
Chưa xem phần 2? Xem (Link) Trong bài viết này tôi giới thiệu cho các bạn về khái niệm function arity, một cách gọi mĩ miều của số lượng argument ...
Lơi Rệ viết 3 năm trước
8 6
{{like_count}}

kipalog

{{ comment_count }}

bình luận

{{liked ? "Đã kipalog" : "Kipalog"}}


White
{{userFollowed ? 'Following' : 'Follow'}}
45 bài viết.
431 người follow

 Đầu mục bài viết

Vẫn còn nữa! x

Kipalog vẫn còn rất nhiều bài viết hay và chủ đề thú vị chờ bạn khám phá!