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

Charly 程式語言

Leonard Schütz

這篇文章是我們客座作家系列的第一篇。如果您使用 Crystal 建構了很棒的東西,並想在這裡的部落格分享您的經驗,請告訴我們

今天的客座作者是 Leonard Schütz。他創建了 Charly 程式語言,作為學習如何創建程式語言的方法,在用 Ruby 進行第一次迭代之後,他轉向使用 Crystal 來實現語言解譯器。在這篇文章中,他介紹了這種語言,展示了它的運作方式,以及為什麼他選擇 Crystal 來實現它。

簡介

Charly 是一種動態型別且物件導向的程式語言。語法主要受到 JavaScript 或 Ruby 等語言的啟發,但在編寫時提供了更多自由。人們可能注意到的第一個差異是缺少分號,或者在大多數語言的控制結構中不需要括號。Charly 現在只是一種在閒暇時間編寫的玩具語言。

它看起來如何?

以下是以 Charly 編寫的 氣泡排序演算法的實作。它是標準函式庫的一部分,標準函式庫也是以 Charly 編寫的。

  func sort(sort_function) {
    const sorted = @copy()

    let left
    let right

    @length().times(->(i) {
      (@length() - 1).times(->(y) {

        left = sorted[i]
        right = sorted[y]

        if sort_function(left, right) {
          sorted[i] = right
          sorted[y] = left
        }

      })
    })

    sorted
  }

這個程式會在一個 60x180 大小的方塊中印出 曼德博集合

60.times(func(a) {
  180.times(func(b) {
    let x = 0
    let y = 0
    let i = 0

    while !(x ** 2 + y ** 2 > 4 || i == 99) {
      x = x ** 2 - y ** 2 + b / 60 - 1.5
      y = 2 * x * y + a / 30 - 1
      i += 1
    }

    if i == 99 {
      write("#")
    } else if i <= 10 {
      write(" ")
    } else {
      write(".")
    }
  })

  write("\n")
})

這個連結會帶您到一個完全以 Charly 編寫的表達式剖析器/解譯器。它支援整數值的加法和乘法。

它是如何運作的?

首先,Charly 會將原始檔轉換為權杖列表。權杖基本上只是一個具有型別的字串。一個簡單的 hello-world 程式可能包含以下權杖

$ cat test/debug.ch
print("Hello World")
$ charly test/debug.ch -f lint -f tokens
1:1:5     │ Identifier  │ print
1:6:1     │ LeftParen   │ (
1:7:13    │ String      │ "Hello World"
1:20:1    │ RightParen  │ )
1:21:1    │ Newline     │
2:1:1     │ EOF         │

程式的這一部分稱為詞法分析器(Lexical Analysis)。它將原始碼轉換為邏輯字元群組。例如,print 識別碼現在是類型為 Identifier 的權杖,其中包含字串 print。列印的文字也是如此。它現在是類型為 String 的權杖,其中包含字串 Hello World。

將整個程式轉換為權杖列表後,剖析器會將它們轉換為 AST(抽象語法樹)。AST 是一種將程式表示為樹狀結構的方法。每個節點都有一個型別和 0 個或多個子節點。表達式 1 + 2 * 3 會產生一個如下所示的 AST

更複雜的東西(例如物件上的方法呼叫)可能看起來像這樣

一旦整個程式轉換為 AST,解譯器就會開始遞迴地遍歷此結構。此程序遵循 Visitor 模式,將 AST 和語言邏輯分開。

BinaryExpression 作為範例。它具有三個屬性。表達式的兩個值和一個運算子。此運算子可以是加號、減號或語言支援的任何其他運算子。它首先解析兩個值,檢查正在使用哪個運算子,然後將其應用於兩個值。根據哪個值在哪一側,此程序可能會產生完全不同的結果。3 + [1, 2][1, 2] + 3NAN[1, 2, 3])不同。

IdentifierLiteral 會從目前範圍載入值,CallExpression 會調用預先定義的函式,依此類推。

為什麼選擇 Crystal?

在這個專案中使用 Crystal 的主要原因是速度和簡潔性。

Crystal 的語法和標準函式庫都受到 Ruby 的啟發。這表示您可以在 Crystal 專案中重複使用 Ruby 世界中的許多知識以及既定的原則和實務。許多 API 詳細資訊都非常相似。例如,如果您無法找到任何關於如何在 Crystal 中開啟檔案的資訊,您甚至可以在 Google 上搜尋「Ruby 開啟檔案」,並且您會發現 StackOverflow 上的第一個答案是 100% 有效的 Crystal 程式碼。當然,這不適用於更複雜的事情,但您始終可以將其作為靈感來源。

關於 Crystal 的另一個優點是,您不必處理許多低階的東西。Crystal 的標準函式庫會處理您認為是低階的大部分事情,甚至是記憶體管理。如果您真的需要執行低階的東西,您可以存取原始指標C 的繫結。這也是 Crystal 中實作正規表示式常值的方式。在內部,它會繫結到 PCRE 函式庫,並在其上放置一個易於使用的抽象層。Crystal 不會重新發明輪子,而是會繫結到現有的 C 函式庫。Crystal 還會繫結到 C 標準函式庫、OpenSSLLibGMPLibXML2LibYAML 以及許多其他函式庫。

切換到 Crystal 的另一個主要原因是速度。Crystal 的速度快得驚人。舊的 Charly 實作使用 Ruby 2.3,僅剖析單個檔案就花了超過 300 毫秒。此外,執行測試套件大約需要 1.8 秒。以 Crystal 編寫並使用 --release 選項編譯的程式,完成所有這些只需要大約 1-2 毫秒!非常令人印象深刻。

由於 Crystal 使用 LLVM,因此您的程式會經過其所有的最佳化傳遞。這些包括但不限於:常數摺疊、無效程式碼消除、函式內聯,甚至在編譯時評估完整的程式碼分支。您無法在 Ruby 中獲得這種效果 ;)

我花了大約一個星期的時間用 Crystal 重寫了大部分解譯器,只有少量的錯誤修復和對標準函式庫的更改花費了更長的時間。

在 Crystal 中開發的另一個優點是,編譯器本身也是用 Crystal 編寫的。這表示 Crystal 是自我託管的。在許多情況下,我從 Crystal 的編譯器複製程式碼並將其修改為我自己的用途。例如,Crystal 的剖析器和詞法分析器在理解這些東西如何運作方面真的很有幫助(我之前從未編寫過剖析器和詞法分析器)。

巨集系統

巨集系統在許多地方都非常方便。它主要用於避免樣板程式碼,並遵循 DRY 模式。

如需如何使用巨集系統的真實範例,請查看以下檔案

例如,Crystal 的標準函式庫使用巨集來提供 property 方法。您可以使用它來避免在將新的實例變數引入到類別時產生樣板程式碼。

結論

在目前的狀態下,Charly 對我來說只是一個學習專案。目前,我不建議將它用於任何嚴肅的事情,除了作為學習如何自己編寫解譯器的資源。Charly 正在 GitHub 上開發,因此隨時可以開啟任何問題、提出新功能,甚至發送您自己的提取請求。也歡迎在本文的評論中提供回饋。

我從 2016 年 8 月左右開始使用 Crystal,並且完全愛上了它。它是我曾經編寫過程式碼的最具表現力且回報最高的語言之一。如果您之前沒有使用過 Crystal,您現在應該試試看。

關於作者

我的名字是 Leonard Schütz,我是來自瑞士的 16 歲學生。我目前在西門子醫療保健領域擔任學徒,主要使用 PHP、EWS 和其他 Web 技術。在閒暇時間,我喜歡從事副專案,而 Charly 程式語言是我目前正在進行的專案。

歡迎追蹤我的 TwitterGitHub 或查看我的網站 leonardschuetz.ch

感謝您的閱讀!