Tokenization

一段 Ruby 程式從你用 ruby 執行到他運作結束之間,一共會被轉換三次,分別是 TokenizeParseCompile 三個過程

第一次轉換:Tokenize

第一次的轉換是所謂的 Tokenize,在這個階段 Ruby 會把你的程式碼轉成 Tokens,也就是把它拆成最小單位的結構,並替他做基本的分類,例如

10.times do |n|  

就會被拆成:

integer: "10" period: "." identifier: "times" keyword: "do" operator: "|" identifier: "n" operator: "|"

這邊以及下面的 Token 不是實際上 Ruby 會產出的 Token,只是方便解釋用

Tokenize 的運作

Tokenize 進行的方式其實很簡單,Ruby 會先把所有程式碼視為純文字,然後從第一個字開始讀取你的程式碼:

10.times do |n|  

1 0.times do |n|

當他讀取到 1 時,Ruby 會認為他目前讀取到一個數字的開頭

10. times do |n|

接著他繼續讀取到 0,然後是 .。這時 Ruby 仍會把目前讀取到的字元們視為一組數字,因為 . 可能是浮點數的小數點。

integer: "10" .times do |n|

但是當他讀取到下一個字元 t 的時候,他會往回退到 .的位置,並把前面的 10 轉換成他發現的第一個 token,接著繼續讀取剩下的字元

integer: "10" period: "." times do |n|

integer: "10" period: "." identifier: "times" do |n|

(Identifier 代表的是 Ruby 中的非保留字,通常代表變數、method 或是 class 名稱)

integer: "10" period: "." identifier: "times" keyword: "do" |n|

接著 Ruby 讀取到了保留字 do,並且把它轉成 keyword token。保留字是在程式語言中帶有特殊意義的單字,所以你沒辦法將它們作為一般的 identifier 使用,例如用來定義 local variable,但可以用作 method 或是變數名稱。(在 Ruby 的 Source Code 裡面有一張表定義了 Ruby 中所有的保留字,應該是這)。

最後,Tokenize 的結果就會像一開始提到的一樣:

integer: "10" period: "." identifier: "times" keyword: "do" operator: "|" identifier: "n" operator: "|"

而實際上 Ruby 產生的 tokens 長得會像這樣:

[[[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, "|"]]

接著 Ruby 會繼續掃描每一個字元,直到他讀取完整份文件為止,而這就是 Ruby 第一次解構重組你的程式碼,下一步就會進到 Parse 的階段,把產生的 Tokens 轉換成有意義的句子。

Experiment 1-1

這一章的 Experiment 為使用 Ripper 來觀察 Ruby 實際產出 Token 的結果,範例程式在此

首先第一個範例為

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"]]

每一行都代表一個 Ruby 解析出的 token,每個 Array 代表的是該 token 所在的位置,symbol 則是 token 的類型,最後就是 token 對應的字元。而比較有趣的地方是,Tokenization 並不會檢查你的 syntax 有沒有錯誤,他只管把純文字轉換成 Token 而已

例如我們把範例程式改成這樣

require "ripper"  
require "pp"

code = <<STR  
10.times do |n  
  puts n
end  
STR

puts code  
pp Ripper.lex(code)  

我們還是可以得到 Tokens

 ......
 [[1, 11], :on_sp, " "],
 [[1, 12], :on_op, "|"],
 [[1, 13], :on_ident, "n"],
 [[1, 15], :on_ignored_nl, "\n"],
 [[2, 0], :on_sp, "  "],
 .......

也就是說實際上會檢查 Syntax 的步驟是 Parsing,這部分會在下一篇文章繼續做說明

Author

Stan Luo

I'm Stan, a junior web developer. I love writing ruby program and rails application.And I'm also looking forward making some contribution the ruby community.

Posted this article on Ruby Under a Microscope, Ruby
comments powered by Disqus