跳到内容
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         │

程序的这部分称为词法分析器(词法分析)。它将源代码转换为字符的逻辑组。例如,print 标识符现在是一个类型为 Identifier 的令牌,包含字符串 print。文本也是如此,它被打印出来。它现在是一个类型为 String 的令牌,包含字符串 Hello World。

在整个程序转换为令牌列表后,解析器将它们转换为 AST(抽象语法树)。AST 是一种以树状结构表示程序的方式。每个节点都有一个类型和 0 个或多个子节点。表达式 1 + 2 * 3 将生成一个 AST,看起来像这样

更复杂的东西,比如对象上的方法调用,可能看起来像这样

一旦整个程序被转换为 AST,解释器就开始递归地遍历这个结构。这个过程遵循 访问者模式 来分离 AST 和语言逻辑。

BinaryExpression 为例。它有三个属性。表达式的两个值和一个运算符。这个运算符可以是加号、减号或语言支持的任何其他运算符。它首先解析两个值,检查正在使用的运算符,并将其应用于这两个值。根据哪个值位于哪一边,这个过程可能会产生完全不同的结果。3 + [1, 2][1, 2] + 3 (NAN[1, 2, 3]) 不同。

一个 IdentifierLiteral 将从当前作用域加载一个值,一个 CallExpression 将调用一个预定义的函数,等等。

为什么选择 Crystal?

使用 Crystal 进行此项目的主要原因是速度和简洁性。

Crystal 的语法和标准库都受到 Ruby 的启发。这意味着你可以将 Ruby 世界中的许多知识、既定原则和实践重新用于 Crystal 项目中。许多 API 细节非常相似。例如,如果你无法找到关于如何在 Crystal 中打开文件的任何信息,你甚至可以搜索“Ruby open file”,你就会发现 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,只有少量 bug 修复和标准库的更改花费了更长的时间。

在 Crystal 中进行开发的另一个好处是,编译器本身也是用 Crystal 编写的。这意味着 Crystal 是自托管的。我多次从 Crystal 的编译器中复制代码并将其改编为自己的用途。例如,Crystal 的解析器和词法分析器对理解这些内容的工作原理非常有帮助(我以前从未编写过解析器和词法分析器)。

宏系统

宏系统在很多地方都非常方便。它主要用于避免样板代码并遵循 DRY 模式。

有关如何使用宏系统的实际示例,请查看以下文件

例如,Crystal 的标准库使用宏来提供 property 方法。你可以使用它来避免在向你的类中引入新实例变量时出现样板代码。

结论

就目前而言,Charly 只是我自己的一个学习项目。目前,我不建议将它用于除学习如何自己编写解释器以外的任何严肃目的。Charly 正在 GitHub 上开发,所以请随时提出任何问题,建议新功能,甚至发送你自己的 pull 请求。也欢迎你在这篇文章的评论中反馈。

我从 2016 年 8 月左右开始使用 Crystal,并且完全爱上了它。它是我曾经编写过的最具表现力和最有益的语言之一。如果你以前没有使用过 Crystal,你应该现在就开始尝试。

关于作者

我叫 Leonard Schütz,是一名来自瑞士的 16 岁学生。我现在是西门子医疗保健领域的学徒,主要从事 PHP、EWS 和其他 Web 技术方面的工作。在我的业余时间,我喜欢做一些副业,其中 Charly 编程语言是我目前正在进行的项目。

请随时在 TwitterGitHub 上关注我,或访问我的网站 leonardschuetz.ch

感谢阅读!