跳至內容
GitHub 儲存庫 論壇 RSS 新聞訂閱

Crystal 的直譯器 – 一份非常特別的節日禮物

Beta Ziliani

期待已久的 Crystal 直譯器已經合併了。要使用它,您需要使用特殊標誌編譯 Crystal,並且在撰寫本文時,官方發布版本(.deb、.rpm、docker 映像檔等)並未與之一起編譯。

這篇文章也兼作此特殊功能的常見問題解答。讓我們從頭開始

為什麼 Crystal 需要直譯器

雖然許多人會覺得這很明顯,但指出直譯器可能啟用的兩個特定功能很有用

  1. 原則上,直譯器應該更快地開始執行程式碼,因為會跳過程式碼生成階段(見下文),從而允許快速測試一些程式碼的結果,而無需重新編譯。應該注意的是,直譯器對於只需要執行一次的短程式來說很快,但編譯模式提供更高的效能,應該是大多數生產用例的首選。此外,雖然直譯程式和編譯程式的行為在某些方面可能有所不同(由於系統的關係),但我們打算將差異保持在最低限度,並且大多數程式在直譯和編譯模式下的行為應該完全相同。
  2. 直譯器改善了除錯體驗,成為該語言的專用工具(不同於通用的 lldb)。

目前的狀態如何?

直譯器目前處於實驗階段,有許多缺失的部分。在這個早期階段合併它的原因是為了能夠正確討論與直譯器相關的 PR 並加快其開發速度。對於那些願意嘗試的人,我們歡迎與直譯器相關的問題,同時考慮到上面連結的已知問題。

它是如何運作的?

原始的 PR 回答了這個問題

當在直譯模式下執行時,會像往常一樣進行語義分析,但不是使用 LLVM 來生成程式碼,而是將程式碼編譯為位元組碼(在此 PR 中定義的自訂位元組碼,與 LLVM 完全無關)。然後有一個直譯器可以理解這個位元組碼。

我該如何調用它?

⚠️ 這不是一成不變的!

假設您已編譯 Crystal 並將 interpreter=1 標誌傳遞給 make,您現在可以使用兩種模式調用直譯器

  • crystal i file.cr

此命令在直譯模式下執行檔案,因此如果 write_hello.cr 包含以下內容

File.write("/tmp/hello", "Hello from the interpreter!")
puts "done"

調用 crystal i write_hello.cr 將會產生檔案 /tmp/hello 並將 "done" 列印到 stdout

  • crystal i

此命令會開啟一個互動式 Crystal 工作階段 (REPL),類似於 Ruby 的 irb。在此工作階段中,我們可以撰寫命令並取得其結果

icr:1:0> File.read_lines("/tmp/hello")
=> ["Hello from the interpreter!"]

(注意:重點標示尚未成為直譯器的一部分。)

除錯程式

在這兩種模式中的任何一種模式下,您都可以在程式碼中使用 debugger 在該點除錯。這類似於 Ruby 的 pry。在那裡您可以使用這些命令(也類似於 pry

  • step:移至下一行/指令,可能會進入方法內部。
  • next:移至下一行/指令,不會進入方法內部。
  • finish:結束目前的方法。
  • continue:恢復執行。
  • whereami:顯示除錯器所在的位置。

例如,如果我們在 writeputs 之間加入 debuggerwrite_hello.cr 中,我們在直譯檔案後會得到以下結果

From: /tmp/write_hello.cr:3:6 <Program>#/tmp/write_hello.cr:

    1: File.write("/tmp/hello", "Hello from the interpreter!")
    2: debugger
 => 3: puts "done"

pry>

如果我們 step,我們可以從標準函式庫中看到正在呼叫的方法

pry> step
From: /Users/beta/projects/crystal/crystal/src/kernel.cr:385:1 <Program>#puts:

    380:
    381: # Prints *objects* to `STDOUT`, each followed by a newline character unless
    382: # the object is a `String` and already ends with a newline.
    383: #
    384: # See also: `IO#puts`.
 => 385: def puts(*objects) : Nil
    386:   STDOUT.puts *objects
    387: end
    388:
    389: # Inspects *object* to `STDOUT` followed
    390: # by a newline. Returns *object*.

pry>

此時,我們可能會很好奇:我們傳遞給此方法的 objects 是什麼?我們再次 step 一次,讓變數在範圍內,然後我們發出

pry> objects
{"done"}

直譯器載入程式的速度有多快?

我們絕對缺少基準,更何況沒有多少 shards 可以成功直譯。在來自標準函式庫和 Crystal Patterns 的一些隨機檔案上進行測試,它載入它們(並執行它們,見下文)的速度比編譯它們時快 50% 到 75%(比較 time crystal <file>time crystal i <file>)。這很大程度取決於 Crystal 在編譯器和直譯器的常見步驟(例如解析和語義分析)上所花費的時間。

它的執行速度有多快(或多慢)?

正如其創作者 Ary 在展示Crystal Conf 1.0 中的那樣,它的執行速度相當快——對於一個直譯器來說。當然,它不如像 Ruby 那樣成熟的直譯器實作效率高,但在我們的初步測試中,它的執行速度足夠快,可以滿足預期的用例。例如,它可以在一秒內處理數百萬個整數加法。也就是說,強烈不建議使用直譯器來處理密集型任務,因為那是編譯程式的任務。


總而言之,合併 PR 是在 Crystal 中獲得可正常運作的直譯器的第一步,更重要的是,它證明了 Crystal 團隊致力於改善開發人員體驗的意願。